16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-21 11:19:19 +02:00

EU7 scenery format and models streaming

This commit is contained in:
maj00r
2026-06-13 15:43:44 +02:00
parent f07c8fdcd2
commit 876a4c7c18
171 changed files with 27924 additions and 281 deletions

View File

@@ -0,0 +1,117 @@
#pragma once
// PUWG 1992 ↔ współrzędne sceny MaSzyny (jak terenAI GeoToSim).
// Master offset = pierwsza napotkana dyrektywa //$g w pliku sceny.
#include <eu07/parser.hpp>
#include <eu07/scene/node/types.hpp>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
namespace eu07::geo {
struct PuwgMasterOffset {
double eastM = 0.0; // wschód [m]
double northM = 0.0; // północ [m]
std::size_t sourceLine = 0;
};
[[nodiscard]] inline double normalizePuwgKmToMeters(const double value) noexcept {
return std::abs(value) < 10000.0 ? value * 1000.0 : value;
}
[[nodiscard]] inline std::optional<PuwgMasterOffset> parseGeoMapLine(std::string_view line) {
std::string trimmed(line);
while (!trimmed.empty() && (trimmed.back() == '\r' || trimmed.back() == '\n')) {
trimmed.pop_back();
}
std::stringstream ss(trimmed);
std::string tag;
std::string system;
double east = 0.0;
double north = 0.0;
if (!(ss >> tag >> system >> east >> north)) {
return std::nullopt;
}
if (tag != "//$g") {
return std::nullopt;
}
PuwgMasterOffset out;
out.eastM = normalizePuwgKmToMeters(east);
out.northM = normalizePuwgKmToMeters(north);
return out;
}
[[nodiscard]] inline std::optional<PuwgMasterOffset> readFirstGeoMapFromFile(
const std::filesystem::path& scnPath) {
std::ifstream in(scnPath);
if (!in) {
return std::nullopt;
}
std::string line;
for (std::size_t lineNo = 0; std::getline(in, line); ++lineNo) {
if (line.find("//$g") == std::string::npos) {
continue;
}
if (const std::optional<PuwgMasterOffset> parsed = parseGeoMapLine(line)) {
PuwgMasterOffset out = *parsed;
out.sourceLine = lineNo + 1;
return out;
}
}
return std::nullopt;
}
[[nodiscard]] inline std::optional<PuwgMasterOffset> readFirstGeoMap(
const std::filesystem::path& scnPath,
const std::vector<StarterDirective>& starters) {
for (const StarterDirective& s : starters) {
if (s.id != StarterId::GeoMap) {
continue;
}
const std::string synthetic = std::string("//$g ") + s.value;
if (const std::optional<PuwgMasterOffset> parsed = parseGeoMapLine(synthetic)) {
PuwgMasterOffset out = *parsed;
out.sourceLine = s.line + 1;
return out;
}
}
return readFirstGeoMapFromFile(scnPath);
}
struct PuwgPoint {
double east = 0.0;
double north = 0.0;
double height = 0.0;
};
[[nodiscard]] inline scene::Vec3 geoToSim(
const double north,
const double east,
const double height,
const PuwgMasterOffset& master) noexcept {
return {
master.eastM - east,
height,
north - master.northM,
};
}
[[nodiscard]] inline PuwgPoint simToGeo(const scene::Vec3& sim, const PuwgMasterOffset& master) noexcept {
return {
master.eastM - sim.x,
sim.z + master.northM,
sim.y,
};
}
} // namespace eu07::geo

View File

@@ -0,0 +1,86 @@
#pragma once
#include <eu07/scene/node/types.hpp>
#include <cmath>
#include <cstddef>
#include <unordered_map>
#include <vector>
namespace eu07::geo {
template <int CellSizeM>
class SpatialGrid {
public:
struct ItemBounds {
double minX = 0.0;
double maxX = 0.0;
double minZ = 0.0;
double maxZ = 0.0;
};
private:
struct CellKey {
int x = 0;
int z = 0;
[[nodiscard]] bool operator==(const CellKey& other) const noexcept {
return x == other.x && z == other.z;
}
};
struct CellKeyHash {
[[nodiscard]] std::size_t operator()(const CellKey& key) const noexcept {
return static_cast<std::size_t>(key.x * 73856093) ^
static_cast<std::size_t>(key.z * 19349663);
}
};
std::unordered_map<CellKey, std::vector<std::size_t>, CellKeyHash> grid_;
[[nodiscard]] static int cellIndex(const double value) noexcept {
return static_cast<int>(std::floor(value / static_cast<double>(CellSizeM)));
}
public:
void clear() { grid_.clear(); }
void insert(const std::size_t itemIndex, const ItemBounds& bounds) {
const int minIx = cellIndex(bounds.minX);
const int maxIx = cellIndex(bounds.maxX);
const int minIz = cellIndex(bounds.minZ);
const int maxIz = cellIndex(bounds.maxZ);
for (int x = minIx; x <= maxIx; ++x) {
for (int z = minIz; z <= maxIz; ++z) {
grid_[{x, z}].push_back(itemIndex);
}
}
}
void query(
const scene::Vec3& point,
const double maxDist,
std::vector<std::size_t>& out) const {
out.clear();
const int cx = cellIndex(point.x);
const int cz = cellIndex(point.z);
int range = static_cast<int>(std::ceil(maxDist / static_cast<double>(CellSizeM)));
if (range < 1) {
range = 1;
}
for (int dz = -range; dz <= range; ++dz) {
for (int dx = -range; dx <= range; ++dx) {
const auto it = grid_.find({cx + dx, cz + dz});
if (it == grid_.end()) {
continue;
}
for (const std::size_t idx : it->second) {
out.push_back(idx);
}
}
}
}
};
} // namespace eu07::geo

View File

@@ -0,0 +1,323 @@
#pragma once
// Jednorazowy indeks nagłówków plików ASC (NMT1) — bbox w PUWG, bez wczytywania siatki.
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>
namespace eu07::nmt {
inline constexpr std::string_view kAscIndexMagic = "# parser_nmt_index v1";
struct AscHeader {
int ncols = 0;
int nrows = 0;
double xll = 0.0;
double yll = 0.0;
double cellsize = 0.0;
double nodata = -9999.0;
};
struct AscIndexEntry {
std::filesystem::path relativePath;
std::int64_t mtimeUtc = 0;
AscHeader header{};
double northMin = 0.0;
double northMax = 0.0;
double eastMin = 0.0;
double eastMax = 0.0;
};
struct AscIndex {
std::filesystem::path catalogRoot;
std::vector<AscIndexEntry> entries;
};
[[nodiscard]] inline std::filesystem::path defaultIndexPath(const std::filesystem::path& catalogRoot) {
return catalogRoot / ".parser_nmt_index.tsv";
}
[[nodiscard]] inline std::int64_t fileMtimeUtc(const std::filesystem::path& path) {
std::error_code ec;
const auto ftime = std::filesystem::last_write_time(path, ec);
if (ec) {
return 0;
}
const auto sctp = std::chrono::time_point_cast<std::chrono::seconds>(
ftime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now());
return std::chrono::duration_cast<std::chrono::seconds>(sctp.time_since_epoch()).count();
}
[[nodiscard]] inline bool parseAscHeaderLine(
std::string_view line,
std::string_view key,
double& outValue) {
if (!line.starts_with(key)) {
return false;
}
const std::size_t pos = line.find_first_of(" \t");
if (pos == std::string_view::npos) {
return false;
}
std::string_view tail = line.substr(pos);
while (!tail.empty() && (tail.front() == ' ' || tail.front() == '\t')) {
tail.remove_prefix(1);
}
try {
outValue = std::stod(std::string(tail));
return true;
} catch (...) {
return false;
}
}
[[nodiscard]] inline bool parseAscHeaderLine(
std::string_view line,
std::string_view key,
int& outValue) {
double tmp = 0.0;
if (!parseAscHeaderLine(line, key, tmp)) {
return false;
}
outValue = static_cast<int>(tmp);
return true;
}
[[nodiscard]] inline std::optional<AscHeader> readAscHeaderOnly(const std::filesystem::path& path) {
std::ifstream in(path);
if (!in) {
return std::nullopt;
}
AscHeader header;
std::string line;
int parsedLines = 0;
while (parsedLines < 6 && std::getline(in, line)) {
if (line.empty()) {
continue;
}
if (parseAscHeaderLine(line, "ncols", header.ncols) ||
parseAscHeaderLine(line, "NCOLS", header.ncols)) {
++parsedLines;
continue;
}
if (parseAscHeaderLine(line, "nrows", header.nrows) ||
parseAscHeaderLine(line, "NROWS", header.nrows)) {
++parsedLines;
continue;
}
if (parseAscHeaderLine(line, "xllcorner", header.xll) ||
parseAscHeaderLine(line, "XLLCORNER", header.xll) ||
parseAscHeaderLine(line, "xllcenter", header.xll) ||
parseAscHeaderLine(line, "XLLCENTER", header.xll)) {
++parsedLines;
continue;
}
if (parseAscHeaderLine(line, "yllcorner", header.yll) ||
parseAscHeaderLine(line, "YLLCORNER", header.yll) ||
parseAscHeaderLine(line, "yllcenter", header.yll) ||
parseAscHeaderLine(line, "YLLCENTER", header.yll)) {
++parsedLines;
continue;
}
if (parseAscHeaderLine(line, "cellsize", header.cellsize) ||
parseAscHeaderLine(line, "CELLSIZE", header.cellsize)) {
++parsedLines;
continue;
}
if (parseAscHeaderLine(line, "NODATA_value", header.nodata) ||
parseAscHeaderLine(line, "nodata_value", header.nodata) ||
parseAscHeaderLine(line, "NODATA", header.nodata)) {
++parsedLines;
continue;
}
}
if (header.ncols <= 0 || header.nrows <= 0 || header.cellsize <= 0.0) {
return std::nullopt;
}
return header;
}
[[nodiscard]] inline AscIndexEntry makeIndexEntry(
const std::filesystem::path& catalogRoot,
const std::filesystem::path& ascPath,
const AscHeader& header) {
AscIndexEntry entry;
entry.relativePath = std::filesystem::relative(ascPath, catalogRoot);
entry.mtimeUtc = fileMtimeUtc(ascPath);
entry.header = header;
// Konwencja ESRI ASC + PUWG (jak terenAI): xll/yll = easting/northing rogu siatki.
entry.northMin = header.yll;
entry.northMax = header.yll + header.nrows * header.cellsize;
entry.eastMin = header.xll;
entry.eastMax = header.xll + header.ncols * header.cellsize;
return entry;
}
[[nodiscard]] inline bool bboxIntersects(
const double northMin,
const double northMax,
const double eastMin,
const double eastMax,
const AscIndexEntry& entry) {
return entry.northMax >= northMin && entry.northMin <= northMax && entry.eastMax >= eastMin &&
entry.eastMin <= eastMax;
}
[[nodiscard]] inline AscIndex buildAscIndex(const std::filesystem::path& catalogRoot) {
AscIndex index;
index.catalogRoot = std::filesystem::absolute(catalogRoot);
std::error_code ec;
if (!std::filesystem::is_directory(index.catalogRoot, ec)) {
throw std::runtime_error("Katalog NMT nie istnieje: " + index.catalogRoot.string());
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(
index.catalogRoot, std::filesystem::directory_options::skip_permission_denied, ec)) {
if (ec) {
break;
}
if (!entry.is_regular_file()) {
continue;
}
const std::filesystem::path path = entry.path();
const std::string ext = path.extension().string();
if (ext != ".asc" && ext != ".ASC") {
continue;
}
if (path.filename() == ".parser_nmt_index.tsv") {
continue;
}
const std::optional<AscHeader> header = readAscHeaderOnly(path);
if (!header) {
continue;
}
index.entries.push_back(makeIndexEntry(index.catalogRoot, path, *header));
}
return index;
}
inline void writeAscIndex(const AscIndex& index, const std::filesystem::path& outPath) {
std::ofstream out(outPath);
if (!out) {
throw std::runtime_error("Nie mozna zapisac indeksu: " + outPath.string());
}
out << kAscIndexMagic << '\n';
out << "# catalog=" << index.catalogRoot.string() << '\n';
out << "# path\tmtime\tncols\tnrows\txll\tyll\tcellsize\tnodata\tnorth_min\tnorth_max\teast_min\teast_max\n";
for (const AscIndexEntry& e : index.entries) {
out << e.relativePath.generic_string() << '\t' << e.mtimeUtc << '\t' << e.header.ncols << '\t'
<< e.header.nrows << '\t' << e.header.xll << '\t' << e.header.yll << '\t' << e.header.cellsize
<< '\t' << e.header.nodata << '\t' << e.northMin << '\t' << e.northMax << '\t' << e.eastMin
<< '\t' << e.eastMax << '\n';
}
}
[[nodiscard]] inline std::optional<AscIndex> loadAscIndex(const std::filesystem::path& indexPath) {
std::ifstream in(indexPath);
if (!in) {
return std::nullopt;
}
std::string line;
if (!std::getline(in, line) || line != kAscIndexMagic) {
return std::nullopt;
}
AscIndex index;
while (std::getline(in, line)) {
if (line.empty() || line.starts_with('#')) {
if (line.starts_with("# catalog=")) {
index.catalogRoot = line.substr(std::string_view("# catalog=").size());
}
continue;
}
std::stringstream ss(line);
AscIndexEntry entry;
std::string rel;
if (!(ss >> rel >> entry.mtimeUtc >> entry.header.ncols >> entry.header.nrows >> entry.header.xll >>
entry.header.yll >> entry.header.cellsize >> entry.header.nodata >> entry.northMin >>
entry.northMax >> entry.eastMin >> entry.eastMax)) {
continue;
}
entry.relativePath = rel;
index.entries.push_back(std::move(entry));
}
return index;
}
[[nodiscard]] inline bool ascIndexEntryStale(
const AscIndexEntry& entry,
const std::filesystem::path& catalogRoot) {
const std::filesystem::path full = catalogRoot / entry.relativePath;
return fileMtimeUtc(full) != entry.mtimeUtc;
}
enum class AscIndexStatus { Loaded, Created, Refreshed };
struct AscIndexResult {
AscIndex index;
AscIndexStatus status = AscIndexStatus::Loaded;
};
[[nodiscard]] inline AscIndexResult loadOrBuildAscIndex(const std::filesystem::path& catalogRoot) {
const std::filesystem::path indexPath = defaultIndexPath(catalogRoot);
if (const std::optional<AscIndex> loaded = loadAscIndex(indexPath)) {
AscIndex index = *loaded;
if (index.catalogRoot.empty()) {
index.catalogRoot = std::filesystem::absolute(catalogRoot);
}
bool stale = false;
for (const AscIndexEntry& e : index.entries) {
if (ascIndexEntryStale(e, index.catalogRoot)) {
stale = true;
break;
}
}
if (!stale) {
return {std::move(index), AscIndexStatus::Loaded};
}
}
const bool hadIndexFile = std::filesystem::exists(indexPath);
AscIndex built = buildAscIndex(catalogRoot);
writeAscIndex(built, indexPath);
return {
std::move(built),
hadIndexFile ? AscIndexStatus::Refreshed : AscIndexStatus::Created,
};
}
[[nodiscard]] inline std::vector<const AscIndexEntry*> selectEntriesForCorridor(
const AscIndex& index,
const double northMin,
const double northMax,
const double eastMin,
const double eastMax) {
std::vector<const AscIndexEntry*> selected;
selected.reserve(index.entries.size());
for (const AscIndexEntry& e : index.entries) {
if (bboxIntersects(northMin, northMax, eastMin, eastMax, e)) {
selected.push_back(&e);
}
}
return selected;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,152 @@
#pragma once
// NMT1 (ASC): wyszukiwanie komorki w promieniu od probki profilu toru.
#include <eu07/geo/puwg1992.hpp>
#include <eu07/nmt/asc_index.hpp>
#include <eu07/nmt/asc_ram.hpp>
#include <eu07/nmt/profile_types.hpp>
#include <cmath>
#include <filesystem>
#include <optional>
#include <vector>
namespace eu07::nmt {
[[nodiscard]] inline const AscIndexEntry* findAscEntryForPoint(
const AscIndex& index,
const double north,
const double east) {
for (const AscIndexEntry& entry : index.entries) {
if (north >= entry.northMin && north <= entry.northMax && east >= entry.eastMin &&
east <= entry.eastMax) {
return &entry;
}
}
return nullptr;
}
namespace detail {
inline bool locateCell(
const AscHeader& header,
const double north,
const double east,
int& outRow,
int& outCol) {
const double northTop = header.yll + header.nrows * header.cellsize;
if (north < header.yll || north > northTop || east < header.xll ||
east > header.xll + header.ncols * header.cellsize) {
return false;
}
outCol = static_cast<int>(std::floor((east - header.xll) / header.cellsize));
outRow = static_cast<int>(std::floor((northTop - north) / header.cellsize));
return outCol >= 0 && outCol < header.ncols && outRow >= 0 && outRow < header.nrows;
}
inline scene::Vec3 cellCenterSim(
const AscHeader& header,
const int row,
const int col,
const geo::PuwgMasterOffset& master) {
const double northTop = header.yll + header.nrows * header.cellsize;
const double north = northTop - (static_cast<double>(row) + 0.5) * header.cellsize;
const double east = header.xll + (static_cast<double>(col) + 0.5) * header.cellsize;
return {
master.eastM - east,
0.0,
north - master.northM,
};
}
} // namespace detail
inline void fillProfileNmtHeights(
std::vector<RouteChainageSample>& samples,
const AscIndex& index,
const std::filesystem::path& catalogRoot,
const geo::PuwgMasterOffset& master,
const double radiusM,
AscTileCache& cache) {
if (radiusM <= 0.0) {
return;
}
const double radiusSq = radiusM * radiusM;
AscTileRam tile;
for (RouteChainageSample& sample : samples) {
const AscIndexEntry* entry =
findAscEntryForPoint(index, sample.geo.north, sample.geo.east);
if (entry == nullptr) {
continue;
}
if (!cache.attachHeights(tile, *entry, catalogRoot)) {
continue;
}
int row0 = 0;
int col0 = 0;
if (!detail::locateCell(tile.header, sample.geo.north, sample.geo.east, row0, col0)) {
continue;
}
const double cell = tile.header.cellsize;
const int cellWindow = static_cast<int>(std::ceil(radiusM / cell)) + 1;
double bestDistSq = radiusSq + 1.0;
std::optional<double> bestHeight;
for (int dr = -cellWindow; dr <= cellWindow; ++dr) {
for (int dc = -cellWindow; dc <= cellWindow; ++dc) {
const int row = row0 + dr;
const int col = col0 + dc;
if (row < 0 || row >= tile.header.nrows || col < 0 || col >= tile.header.ncols) {
continue;
}
const std::optional<double> height = tile.heightAt(row, col);
if (!height) {
continue;
}
const scene::Vec3 cellSim = detail::cellCenterSim(tile.header, row, col, master);
const double dx = cellSim.x - sample.sim.x;
const double dz = cellSim.z - sample.sim.z;
const double distSq = dx * dx + dz * dz;
if (distSq <= radiusSq && distSq < bestDistSq) {
bestDistSq = distSq;
bestHeight = height;
}
}
}
sample.nmtHeight = bestHeight;
}
}
[[nodiscard]] inline ProfileNmtResult processRouteProfileNmt(
const AscIndex& index,
const std::filesystem::path& catalogRoot,
const geo::PuwgMasterOffset& master,
const double radiusM,
std::vector<RouteChainageSample>& samples,
AscTileCache& cache) {
ProfileNmtResult result;
const std::size_t tilesBefore = cache.heightsByPath.size();
fillProfileNmtHeights(samples, index, catalogRoot, master, radiusM, cache);
result.tilesLoaded = cache.heightsByPath.size() - tilesBefore;
for (const RouteChainageSample& sample : samples) {
if (sample.nmtHeight) {
++result.samplesWithNmt;
}
}
return result;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,228 @@
#pragma once
// Kafel ASC w RAM + cache między trasami (lazy load per kafel).
#include <eu07/geo/puwg1992.hpp>
#include <eu07/nmt/asc_index.hpp>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
namespace eu07::nmt {
struct AscTileRam {
AscHeader header{};
std::shared_ptr<const std::vector<float>> heights;
std::filesystem::path relativePath;
[[nodiscard]] std::size_t index(const int row, const int col) const {
return static_cast<std::size_t>(row) * static_cast<std::size_t>(header.ncols) + static_cast<std::size_t>(col);
}
[[nodiscard]] bool validAt(const int row, const int col) const {
if (heights == nullptr || row < 0 || row >= header.nrows || col < 0 || col >= header.ncols) {
return false;
}
const float z = (*heights)[index(row, col)];
return !std::isnan(z);
}
[[nodiscard]] std::optional<double> heightAt(const int row, const int col) const {
if (!validAt(row, col)) {
return std::nullopt;
}
return static_cast<double>((*heights)[index(row, col)]);
}
};
namespace detail {
inline std::shared_ptr<std::vector<float>> makeMutableHeightsBuffer(const AscHeader& header) {
const std::size_t cellCount =
static_cast<std::size_t>(header.nrows) * static_cast<std::size_t>(header.ncols);
return std::make_shared<std::vector<float>>(
cellCount, std::numeric_limits<float>::quiet_NaN());
}
inline std::vector<char> readAscFileBuffer(const std::filesystem::path& ascPath) {
std::ifstream file(ascPath, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
const std::streamsize size = file.tellg();
if (size <= 0) {
return {};
}
file.seekg(0, std::ios::beg);
std::vector<char> buffer(static_cast<std::size_t>(size) + 1);
if (!file.read(buffer.data(), size)) {
return {};
}
buffer[static_cast<std::size_t>(size)] = '\0';
return buffer;
}
inline double fastParseDouble(char** ptr, char* endPtr) {
char* p = *ptr;
while (p < endPtr && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
++p;
}
if (p >= endPtr) {
*ptr = p;
return 0.0;
}
bool neg = false;
if (*p == '-') {
neg = true;
++p;
} else if (*p == '+') {
++p;
}
double val = 0.0;
while (p < endPtr && *p >= '0' && *p <= '9') {
val = val * 10.0 + static_cast<double>(*p - '0');
++p;
}
if (p < endPtr && (*p == '.' || *p == ',')) {
++p;
double frac = 1.0;
while (p < endPtr && *p >= '0' && *p <= '9') {
frac *= 0.1;
val += static_cast<double>(*p - '0') * frac;
++p;
}
}
*ptr = p;
return neg ? -val : val;
}
inline void fastSkipToken(char** ptr, char* endPtr) {
char* p = *ptr;
while (p < endPtr && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
++p;
}
while (p < endPtr && !(*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
++p;
}
*ptr = p;
}
inline bool parseAscHeaderFromBuffer(char*& ptr, char* endPtr, AscHeader& header) {
for (int h = 0; h < 6; ++h) {
fastSkipToken(&ptr, endPtr);
if (h == 0) {
header.ncols = static_cast<int>(fastParseDouble(&ptr, endPtr));
} else if (h == 1) {
header.nrows = static_cast<int>(fastParseDouble(&ptr, endPtr));
} else if (h == 2) {
header.xll = fastParseDouble(&ptr, endPtr);
} else if (h == 3) {
header.yll = fastParseDouble(&ptr, endPtr);
} else if (h == 4) {
header.cellsize = fastParseDouble(&ptr, endPtr);
} else {
header.nodata = fastParseDouble(&ptr, endPtr);
}
}
return header.ncols > 0 && header.nrows > 0 && header.cellsize > 0.0;
}
[[nodiscard]] inline std::optional<AscTileRam> loadAscTileRam(
const AscIndexEntry& entry,
const std::filesystem::path& ascPath) {
std::vector<char> buffer = readAscFileBuffer(ascPath);
if (buffer.empty()) {
return std::nullopt;
}
char* ptr = buffer.data();
char* endPtr = buffer.data() + buffer.size() - 1;
AscTileRam tile;
tile.relativePath = entry.relativePath;
if (!parseAscHeaderFromBuffer(ptr, endPtr, tile.header)) {
return std::nullopt;
}
if (tile.header.ncols > 20000 || tile.header.nrows > 20000) {
return std::nullopt;
}
const std::shared_ptr<std::vector<float>> heights = makeMutableHeightsBuffer(tile.header);
tile.heights = heights;
for (int row = 0; row < tile.header.nrows; ++row) {
for (int col = 0; col < tile.header.ncols; ++col) {
const double value = fastParseDouble(&ptr, endPtr);
const std::size_t idx =
static_cast<std::size_t>(row) * static_cast<std::size_t>(tile.header.ncols) +
static_cast<std::size_t>(col);
if (std::abs(value - tile.header.nodata) <= 0.1) {
(*heights)[idx] = std::numeric_limits<float>::quiet_NaN();
} else {
(*heights)[idx] = static_cast<float>(value);
}
}
}
return tile;
}
} // namespace detail
struct AscTileCache {
std::unordered_map<std::string, std::shared_ptr<const std::vector<float>>> heightsByPath;
std::unordered_map<std::string, AscHeader> headerByPath;
std::size_t ramBytes = 0;
std::mutex mutex;
[[nodiscard]] bool attachHeights(
AscTileRam& tile,
const AscIndexEntry& entry,
const std::filesystem::path& catalogRoot) {
const std::string key = entry.relativePath.generic_string();
{
std::lock_guard lock(mutex);
const auto headerIt = headerByPath.find(key);
const auto heightsIt = heightsByPath.find(key);
if (headerIt != headerByPath.end() && heightsIt != heightsByPath.end()) {
tile.header = headerIt->second;
tile.heights = heightsIt->second;
tile.relativePath = entry.relativePath;
return tile.header.ncols > 0 && tile.header.nrows > 0;
}
}
if (const std::optional<AscTileRam> loaded =
detail::loadAscTileRam(entry, catalogRoot / entry.relativePath)) {
std::lock_guard lock(mutex);
const std::string storeKey = loaded->relativePath.generic_string();
if (const auto heightsIt = heightsByPath.find(storeKey); heightsIt != heightsByPath.end()) {
tile.header = headerByPath[storeKey];
tile.heights = heightsIt->second;
} else {
heightsByPath.emplace(storeKey, loaded->heights);
headerByPath.emplace(storeKey, loaded->header);
ramBytes += loaded->heights->size() * sizeof(float);
tile.header = loaded->header;
tile.heights = loaded->heights;
}
tile.relativePath = loaded->relativePath;
return tile.header.ncols > 0 && tile.header.nrows > 0;
}
return false;
}
};
} // namespace eu07::nmt

View File

@@ -0,0 +1,16 @@
#pragma once
#include <algorithm>
#include <thread>
namespace eu07::nmt {
[[nodiscard]] inline unsigned int workerThreadCount() {
const unsigned int hw = std::thread::hardware_concurrency();
if (hw <= 2) {
return 1;
}
return hw - 1;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,92 @@
#pragma once
#include <eu07/nmt/profile_height.hpp>
#include <eu07/nmt/profile_slope.hpp>
#include <eu07/nmt/profile_types.hpp>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <stdexcept>
#include <vector>
namespace eu07::nmt {
struct ProfileCsvOptions {
bool includeRouteColumn = false;
};
inline void writeProfileCsv(
const std::filesystem::path& outPath,
const std::vector<RouteChainageSample>& samples,
const ProfileCsvOptions& options = {}) {
std::ofstream out(outPath, std::ios::binary);
if (!out) {
throw std::runtime_error("Nie mozna zapisac: " + outPath.string());
}
const std::vector<std::optional<double>> slopes = slopePromilleEvery50m(samples);
out << std::fixed << std::setprecision(3);
if (options.includeRouteColumn) {
out << "route,chainage_m,east,north,height,slope_promille\n";
} else {
out << "chainage_m,east,north,height,slope_promille\n";
}
for (std::size_t i = 0; i < samples.size(); ++i) {
const RouteChainageSample& s = samples[i];
if (options.includeRouteColumn) {
out << s.routeIndex << ',';
}
out << s.chainageM << ',' << s.geo.east << ',' << s.geo.north << ','
<< profileSampleHeightZ(s) << ',';
if (slopes[i]) {
out << *slopes[i];
}
out << '\n';
}
}
inline void writeCombinedProfileCsv(
const std::filesystem::path& outPath,
const std::vector<RouteChainageSample>& samples) {
writeProfileCsv(outPath, samples, {.includeRouteColumn = true});
}
inline void writeCombinedProfileXyz(
const std::filesystem::path& outPath,
const std::vector<RouteChainageSample>& samples) {
std::ofstream out(outPath, std::ios::binary);
if (!out) {
throw std::runtime_error("Nie mozna zapisac: " + outPath.string());
}
out << std::fixed << std::setprecision(1);
for (const RouteChainageSample& s : samples) {
out << s.geo.east << ' ' << s.geo.north << ' ' << profileSampleHeightZ(s) << '\n';
}
}
inline void writeRouteProfileReport(
const std::filesystem::path& outPath,
const std::vector<RouteChainageSample>& samples) {
std::ofstream out(outPath);
if (!out) {
throw std::runtime_error("Nie mozna zapisac: " + outPath.string());
}
out << std::fixed << std::setprecision(3);
out << "# chainage_m east north track_h nmt_h (PUWG1992 / EPSG:2180)\n";
for (const RouteChainageSample& s : samples) {
out << s.chainageM << ' ' << s.geo.east << ' ' << s.geo.north << ' ' << s.trackHeight << ' ';
if (s.nmtHeight) {
out << *s.nmtHeight;
} else {
out << '-';
}
out << '\n';
}
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,14 @@
#pragma once
#include <eu07/nmt/profile_types.hpp>
namespace eu07::nmt {
[[nodiscard]] inline double profileSampleHeightZ(const RouteChainageSample& sample) {
if (sample.nmtHeight) {
return *sample.nmtHeight;
}
return sample.trackHeight;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,102 @@
#pragma once
#include <eu07/nmt/profile_height.hpp>
#include <eu07/nmt/profile_types.hpp>
#include <cmath>
#include <cstddef>
#include <optional>
#include <vector>
namespace eu07::nmt {
[[nodiscard]] inline double horizontalDistanceXY(
const RouteChainageSample& a,
const RouteChainageSample& b) noexcept {
const double dx = b.geo.east - a.geo.east;
const double dy = b.geo.north - a.geo.north;
return std::sqrt(dx * dx + dy * dy);
}
[[nodiscard]] inline std::optional<double> slopePermilleOverChainageSegment(
const std::vector<RouteChainageSample>& samples,
const std::size_t routeStart,
const std::size_t routeEnd,
const double segStartChainage,
const double segEndChainage,
std::optional<std::size_t>& outEndIndex) {
std::optional<std::size_t> startIndex;
std::optional<std::size_t> prevIndex;
double pathLengthM = 0.0;
for (std::size_t i = routeStart; i < routeEnd; ++i) {
if (samples[i].chainageM + 1e-6 < segStartChainage) {
continue;
}
if (samples[i].chainageM > segEndChainage + 1e-6) {
break;
}
if (!startIndex) {
startIndex = i;
}
if (prevIndex) {
pathLengthM += horizontalDistanceXY(samples[*prevIndex], samples[i]);
}
prevIndex = i;
}
if (!startIndex || !prevIndex || *startIndex == *prevIndex || pathLengthM <= 1e-6) {
outEndIndex = std::nullopt;
return std::nullopt;
}
outEndIndex = prevIndex;
const double dh =
profileSampleHeightZ(samples[*prevIndex]) - profileSampleHeightZ(samples[*startIndex]);
return dh / pathLengthM * 1000.0;
}
[[nodiscard]] inline std::vector<std::optional<double>> slopePromilleEvery50m(
const std::vector<RouteChainageSample>& samples) {
std::vector<std::optional<double>> slopes(samples.size());
if (samples.empty()) {
return slopes;
}
constexpr double kSlopeIntervalM = 50.0;
for (std::size_t routeStart = 0; routeStart < samples.size();) {
const int routeIndex = samples[routeStart].routeIndex;
std::size_t routeEnd = routeStart;
while (routeEnd < samples.size() && samples[routeEnd].routeIndex == routeIndex) {
++routeEnd;
}
const double maxChainage = samples[routeEnd - 1].chainageM;
for (double segStart = 0.0; segStart <= maxChainage + 1e-6; segStart += kSlopeIntervalM) {
const double segEndTarget = segStart + kSlopeIntervalM;
const bool isLastSegment = segEndTarget >= maxChainage - 1e-6;
const double segEndChainage = isLastSegment ? maxChainage : segEndTarget;
std::optional<std::size_t> endIndex;
const std::optional<double> slope = slopePermilleOverChainageSegment(
samples,
routeStart,
routeEnd,
segStart,
segEndChainage,
endIndex);
if (slope && endIndex) {
slopes[*endIndex] = *slope;
}
}
routeStart = routeEnd;
}
return slopes;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,25 @@
#pragma once
#include <eu07/geo/puwg1992.hpp>
#include <eu07/scene/node/types.hpp>
#include <cstddef>
#include <optional>
namespace eu07::nmt {
struct RouteChainageSample {
double chainageM = 0.0;
scene::Vec3 sim{};
geo::PuwgPoint geo{};
double trackHeight = 0.0;
std::optional<double> nmtHeight;
int routeIndex = 0;
};
struct ProfileNmtResult {
std::size_t samplesWithNmt = 0;
std::size_t tilesLoaded = 0;
};
} // namespace eu07::nmt

View File

@@ -0,0 +1,52 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <iostream>
#include <string>
namespace eu07::nmt {
class ProgressLine {
public:
void begin(std::string label, const std::size_t total) {
label_ = std::move(label);
total_ = total > 0 ? total : 1;
current_ = 0;
lastPrint_ = std::chrono::steady_clock::now();
render(true);
}
void set(const std::size_t current) {
current_ = current > total_ ? total_ : current;
render(false);
}
void advance(const std::size_t delta = 1) { set(current_ + delta); }
void end() {
set(total_);
std::cerr << '\n';
}
private:
void render(const bool force) {
const auto now = std::chrono::steady_clock::now();
if (!force &&
std::chrono::duration_cast<std::chrono::milliseconds>(now - lastPrint_).count() < 200) {
return;
}
lastPrint_ = now;
const int pct = static_cast<int>((100.0 * static_cast<double>(current_)) / static_cast<double>(total_));
std::cerr << '\r' << label_ << ' ' << current_ << '/' << total_ << " (" << pct << "%) "
<< std::flush;
}
std::string label_;
std::size_t total_ = 1;
std::size_t current_ = 0;
std::chrono::steady_clock::time_point lastPrint_{};
};
} // namespace eu07::nmt

View File

@@ -0,0 +1,462 @@
#pragma once
// Geometria trasy: próbkowanie co N metrów wzdłuż osi toru + odległość boczna (pas 1 m).
#include <eu07/geo/puwg1992.hpp>
#include <eu07/geo/spatial_grid.hpp>
#include <eu07/nmt/profile_types.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/node/track.hpp>
#include <eu07/scene/track_routes.hpp>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
namespace eu07::nmt {
struct TrackLineSegment {
scene::Vec3 p1{};
scene::Vec3 p2{};
};
struct BezierSpan {
scene::Vec3 p1;
scene::Vec3 cv1;
scene::Vec3 cv2;
scene::Vec3 p2;
double lengthM = 0.0;
double chainageStartM = 0.0;
};
struct RouteGeometry {
std::string label;
double totalLengthM = 0.0;
std::vector<BezierSpan> spans;
std::vector<TrackLineSegment> segments;
geo::SpatialGrid<25> grid;
};
struct RouteGeometryOptions {
double distanceSegmentLengthM = 2.0;
int minSegmentsPerBezier = 4;
int maxSegmentsPerBezier = 300;
};
namespace detail {
inline double vecLength(const scene::Vec3& v) noexcept {
return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
inline scene::Vec3 vecAdd(const scene::Vec3& a, const scene::Vec3& b) noexcept {
return {a.x + b.x, a.y + b.y, a.z + b.z};
}
inline scene::Vec3 vecScale(const scene::Vec3& v, const double s) noexcept {
return {v.x * s, v.y * s, v.z * s};
}
inline scene::Vec3 cubicBezier(
const double t,
const scene::Vec3& p1,
const scene::Vec3& cv1,
const scene::Vec3& cv2,
const scene::Vec3& p2) {
const scene::Vec3 c1 = vecAdd(p1, cv1);
const scene::Vec3 c2 = vecAdd(p2, cv2);
const double u = 1.0 - t;
const double tt = t * t;
const double uu = u * u;
const double uuu = uu * u;
const double ttt = tt * t;
scene::Vec3 p = vecScale(p1, uuu);
p = vecAdd(p, vecScale(c1, 3.0 * uu * t));
p = vecAdd(p, vecScale(c2, 3.0 * u * tt));
p = vecAdd(p, vecScale(p2, ttt));
return p;
}
inline double approximateBezierLength(
const scene::Vec3& p1,
const scene::Vec3& cv1,
const scene::Vec3& cv2,
const scene::Vec3& p2) {
const scene::Vec3 c1 = vecAdd(p1, cv1);
const scene::Vec3 c2 = vecAdd(p2, cv2);
return vecLength(cv1) + vecLength(vecAdd(c2, vecScale(c1, -1.0))) + vecLength(cv2);
}
inline void appendBezierSpansAndSegments(
const scene::TrackBezier& bez,
const scene::Vec3& originOffset,
const RouteGeometryOptions& opts,
const double chainageStart,
std::vector<BezierSpan>& spans,
std::vector<TrackLineSegment>& segments,
double& outTotalLength) {
BezierSpan span;
span.p1 = scene::detail::track_routes::addOffset(bez.p1, originOffset);
span.p2 = scene::detail::track_routes::addOffset(bez.p2, originOffset);
span.cv1 = bez.cv1;
span.cv2 = bez.cv2;
span.lengthM = approximateBezierLength(span.p1, span.cv1, span.cv2, span.p2);
span.chainageStartM = chainageStart;
spans.push_back(span);
outTotalLength += span.lengthM;
int pieces = static_cast<int>(span.lengthM / opts.distanceSegmentLengthM);
if (pieces < opts.minSegmentsPerBezier) {
pieces = opts.minSegmentsPerBezier;
}
if (pieces > opts.maxSegmentsPerBezier) {
pieces = opts.maxSegmentsPerBezier;
}
scene::Vec3 prev = span.p1;
for (int i = 1; i <= pieces; ++i) {
const double t = static_cast<double>(i) / static_cast<double>(pieces);
const scene::Vec3 curr = cubicBezier(t, span.p1, span.cv1, span.cv2, span.p2);
segments.push_back({prev, curr});
prev = curr;
}
}
inline const std::vector<scene::TrackBezier>* beziersForSegment(
const scene::SceneDocument& doc,
const scene::TrackSegmentEndpoints& segment) {
switch (segment.ref.kind) {
case scene::TrackSegmentKind::Normal:
return &doc.nodeTrackNormal[segment.ref.index].beziers;
case scene::TrackSegmentKind::Switch:
return &doc.nodeTrackSwitch[segment.ref.index].beziers;
case scene::TrackSegmentKind::Road:
return &doc.nodeTrackRoad[segment.ref.index].beziers;
case scene::TrackSegmentKind::Cross:
return &doc.nodeTrackCross[segment.ref.index].beziers;
case scene::TrackSegmentKind::Other:
return &doc.nodeTrackOther[segment.ref.index].beziers;
}
return nullptr;
}
inline scene::Vec3 headerOffsetForSegment(
const scene::SceneDocument& doc,
const scene::TrackSegmentEndpoints& segment) {
switch (segment.ref.kind) {
case scene::TrackSegmentKind::Normal:
return doc.nodeTrackNormal[segment.ref.index].header.originOffset;
case scene::TrackSegmentKind::Switch:
return doc.nodeTrackSwitch[segment.ref.index].header.originOffset;
case scene::TrackSegmentKind::Road:
return doc.nodeTrackRoad[segment.ref.index].header.originOffset;
case scene::TrackSegmentKind::Cross:
return doc.nodeTrackCross[segment.ref.index].header.originOffset;
case scene::TrackSegmentKind::Other:
return doc.nodeTrackOther[segment.ref.index].header.originOffset;
}
return {};
}
inline scene::Vec3 pointOnBezierSpan(const BezierSpan& span, const double localT) {
return cubicBezier(localT, span.p1, span.cv1, span.cv2, span.p2);
}
} // namespace detail
[[nodiscard]] inline RouteGeometry buildRouteGeometry(
const scene::SceneDocument& doc,
const scene::TrackRouteBuildResult& routes,
const std::size_t routeIndex,
const RouteGeometryOptions& opts = {}) {
RouteGeometry geometry;
if (routeIndex >= routes.routes.size()) {
return geometry;
}
const scene::TrackRoute& route = routes.routes[routeIndex];
geometry.label = route.label;
geometry.spans.reserve(route.links.size() * 4);
geometry.segments.reserve(route.links.size() * 32);
double chainage = 0.0;
for (const scene::TrackRouteLink& link : route.links) {
const scene::TrackSegmentEndpoints& segment = routes.segments[link.segmentIndex];
const std::vector<scene::TrackBezier>* beziers = detail::beziersForSegment(doc, segment);
if (beziers == nullptr || beziers->empty()) {
continue;
}
const scene::Vec3 origin = detail::headerOffsetForSegment(doc, segment);
const std::vector<scene::TrackBezier>& list = *beziers;
if (link.reversed) {
for (auto it = list.rbegin(); it != list.rend(); ++it) {
scene::TrackBezier flipped = *it;
std::swap(flipped.p1, flipped.p2);
std::swap(flipped.cv1, flipped.cv2);
detail::appendBezierSpansAndSegments(
flipped, origin, opts, chainage, geometry.spans, geometry.segments, chainage);
}
} else {
for (const scene::TrackBezier& bez : list) {
detail::appendBezierSpansAndSegments(
bez, origin, opts, chainage, geometry.spans, geometry.segments, chainage);
}
}
}
geometry.totalLengthM = chainage;
for (std::size_t i = 0; i < geometry.segments.size(); ++i) {
const TrackLineSegment& seg = geometry.segments[i];
geo::SpatialGrid<25>::ItemBounds bounds{
std::min(seg.p1.x, seg.p2.x),
std::max(seg.p1.x, seg.p2.x),
std::min(seg.p1.z, seg.p2.z),
std::max(seg.p1.z, seg.p2.z),
};
geometry.grid.insert(i, bounds);
}
return geometry;
}
[[nodiscard]] inline RouteGeometry subsampleRouteGeometryForBand(
const RouteGeometry& source,
const std::size_t everyNthSegment = 4) {
RouteGeometry out;
out.label = source.label;
out.totalLengthM = source.totalLengthM;
if (source.segments.empty() || everyNthSegment == 0) {
return out;
}
out.segments.reserve(source.segments.size() / everyNthSegment + 1);
for (std::size_t i = 0; i < source.segments.size(); i += everyNthSegment) {
out.segments.push_back(source.segments[i]);
}
for (std::size_t i = 0; i < out.segments.size(); ++i) {
const TrackLineSegment& seg = out.segments[i];
geo::SpatialGrid<25>::ItemBounds bounds{
std::min(seg.p1.x, seg.p2.x),
std::max(seg.p1.x, seg.p2.x),
std::min(seg.p1.z, seg.p2.z),
std::max(seg.p1.z, seg.p2.z),
};
out.grid.insert(i, bounds);
}
return out;
}
[[nodiscard]] inline std::vector<RouteChainageSample> sampleRouteCenterlineFromGeometry(
const RouteGeometry& geometry,
const geo::PuwgMasterOffset& master,
const double stepM) {
std::vector<RouteChainageSample> samples;
if (geometry.spans.empty() || geometry.totalLengthM <= 0.0 || stepM <= 0.0) {
return samples;
}
for (double chainage = 0.0; chainage <= geometry.totalLengthM + 1e-6; chainage += stepM) {
const BezierSpan* span = nullptr;
for (const BezierSpan& candidate : geometry.spans) {
if (chainage >= candidate.chainageStartM &&
chainage <= candidate.chainageStartM + candidate.lengthM + 1e-6) {
span = &candidate;
break;
}
}
if (span == nullptr) {
span = &geometry.spans.back();
}
const double local = span->lengthM > 0.0
? std::clamp((chainage - span->chainageStartM) / span->lengthM, 0.0, 1.0)
: 0.0;
const scene::Vec3 sim = detail::pointOnBezierSpan(*span, local);
RouteChainageSample sample;
sample.chainageM = chainage;
sample.sim = sim;
sample.trackHeight = sim.y;
sample.geo = geo::simToGeo(sim, master);
samples.push_back(sample);
}
return samples;
}
[[nodiscard]] inline std::vector<RouteChainageSample> sampleRouteCenterlineEvery(
const scene::SceneDocument& doc,
const scene::TrackRouteBuildResult& routes,
const std::size_t routeIndex,
const geo::PuwgMasterOffset& master,
const double stepM) {
std::vector<RouteChainageSample> samples;
if (routeIndex >= routes.routes.size() || stepM <= 0.0) {
return samples;
}
const scene::TrackRoute& route = routes.routes[routeIndex];
std::vector<BezierSpan> spans;
spans.reserve(route.links.size() * 4);
double totalLength = 0.0;
for (const scene::TrackRouteLink& link : route.links) {
const scene::TrackSegmentEndpoints& segment = routes.segments[link.segmentIndex];
const std::vector<scene::TrackBezier>* beziers = detail::beziersForSegment(doc, segment);
if (beziers == nullptr || beziers->empty()) {
continue;
}
const scene::Vec3 origin = detail::headerOffsetForSegment(doc, segment);
const std::vector<scene::TrackBezier>& list = *beziers;
if (link.reversed) {
for (auto it = list.rbegin(); it != list.rend(); ++it) {
scene::TrackBezier flipped = *it;
std::swap(flipped.p1, flipped.p2);
std::swap(flipped.cv1, flipped.cv2);
BezierSpan span;
span.p1 = scene::detail::track_routes::addOffset(flipped.p1, origin);
span.p2 = scene::detail::track_routes::addOffset(flipped.p2, origin);
span.cv1 = flipped.cv1;
span.cv2 = flipped.cv2;
span.lengthM =
detail::approximateBezierLength(span.p1, span.cv1, span.cv2, span.p2);
span.chainageStartM = totalLength;
spans.push_back(span);
totalLength += span.lengthM;
}
} else {
for (const scene::TrackBezier& bez : list) {
BezierSpan span;
span.p1 = scene::detail::track_routes::addOffset(bez.p1, origin);
span.p2 = scene::detail::track_routes::addOffset(bez.p2, origin);
span.cv1 = bez.cv1;
span.cv2 = bez.cv2;
span.lengthM = detail::approximateBezierLength(span.p1, span.cv1, span.cv2, span.p2);
span.chainageStartM = totalLength;
spans.push_back(span);
totalLength += span.lengthM;
}
}
}
if (spans.empty() || totalLength <= 0.0) {
return samples;
}
for (double chainage = 0.0; chainage <= totalLength + 1e-6; chainage += stepM) {
const BezierSpan* span = nullptr;
for (const BezierSpan& candidate : spans) {
if (chainage >= candidate.chainageStartM &&
chainage <= candidate.chainageStartM + candidate.lengthM + 1e-6) {
span = &candidate;
break;
}
}
if (span == nullptr) {
span = &spans.back();
}
const double local = span->lengthM > 0.0
? std::clamp((chainage - span->chainageStartM) / span->lengthM, 0.0, 1.0)
: 0.0;
const scene::Vec3 sim = detail::pointOnBezierSpan(*span, local);
RouteChainageSample sample;
sample.chainageM = chainage;
sample.sim = sim;
sample.trackHeight = sim.y;
sample.geo = geo::simToGeo(sim, master);
samples.push_back(sample);
}
return samples;
}
[[nodiscard]] inline double distancePointToSegmentXZ(
const scene::Vec3& point,
const TrackLineSegment& seg,
double& outTrackY) noexcept {
const double dx = seg.p2.x - seg.p1.x;
const double dz = seg.p2.z - seg.p1.z;
const double l2 = dx * dx + dz * dz;
if (l2 <= 0.0) {
outTrackY = seg.p1.y;
const double px = point.x - seg.p1.x;
const double pz = point.z - seg.p1.z;
return std::sqrt(px * px + pz * pz);
}
double t = ((point.x - seg.p1.x) * dx + (point.z - seg.p1.z) * dz) / l2;
t = std::max(0.0, std::min(1.0, t));
const double projX = seg.p1.x + t * dx;
const double projZ = seg.p1.z + t * dz;
outTrackY = seg.p1.y + t * (seg.p2.y - seg.p1.y);
const double qx = point.x - projX;
const double qz = point.z - projZ;
return std::sqrt(qx * qx + qz * qz);
}
[[nodiscard]] inline double minDistanceToRouteXZ(
const scene::Vec3& point,
const RouteGeometry& geometry,
const double searchRadius,
std::vector<std::size_t>& scratch) {
geometry.grid.query(point, searchRadius, scratch);
double best = searchRadius + 1.0;
double trackY = 0.0;
for (const std::size_t idx : scratch) {
const double d = distancePointToSegmentXZ(point, geometry.segments[idx], trackY);
if (d < best) {
best = d;
}
}
return best;
}
inline void corridorBboxPUWG(
const RouteGeometry& geometry,
const geo::PuwgMasterOffset& master,
const double marginM,
double& northMin,
double& northMax,
double& eastMin,
double& eastMax) {
if (geometry.segments.empty()) {
northMin = northMax = eastMin = eastMax = 0.0;
return;
}
northMin = 1e18;
northMax = -1e18;
eastMin = 1e18;
eastMax = -1e18;
for (const TrackLineSegment& seg : geometry.segments) {
for (const scene::Vec3& sim : {seg.p1, seg.p2}) {
const geo::PuwgPoint geo = geo::simToGeo(sim, master);
northMin = std::min(northMin, geo.north);
northMax = std::max(northMax, geo.north);
eastMin = std::min(eastMin, geo.east);
eastMax = std::max(eastMax, geo.east);
}
}
northMin -= marginM;
northMax += marginM;
eastMin -= marginM;
eastMax += marginM;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,14 @@
#pragma once
// Zbiorcze include modulow obrobki terenu (NMT + profil toru).
#include <eu07/nmt/asc_index.hpp>
#include <eu07/nmt/asc_lookup.hpp>
#include <eu07/nmt/asc_ram.hpp>
#include <eu07/nmt/profile_export.hpp>
#include <eu07/nmt/profile_height.hpp>
#include <eu07/nmt/profile_slope.hpp>
#include <eu07/nmt/profile_types.hpp>
#include <eu07/nmt/route_geometry.hpp>
#include <eu07/nmt/terrain_cli.hpp>
#include <eu07/nmt/terrain_pipeline.hpp>

View File

@@ -0,0 +1,33 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
namespace eu07::nmt {
struct TerrainProfileOptions {
std::optional<std::filesystem::path> nmtDir;
double profileStepM = 10.0;
double nmtRadiusM = 1.0;
};
[[nodiscard]] inline TerrainProfileOptions parseTerrainProfileCli(int argc, char** argv) {
TerrainProfileOptions opts;
for (int i = 2; i < argc; ++i) {
const std::string_view arg = argv[i];
if (arg == "--nmt-dir" && i + 1 < argc) {
opts.nmtDir = std::filesystem::path{argv[++i]};
} else if (arg == "--step" && i + 1 < argc) {
opts.profileStepM = std::stod(argv[++i]);
} else if (arg == "--band" && i + 1 < argc) {
opts.nmtRadiusM = std::stod(argv[++i]);
} else if (arg == "--radius" && i + 1 < argc) {
opts.nmtRadiusM = std::stod(argv[++i]);
}
}
return opts;
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,102 @@
#pragma once
// Pipeline: geometria toru -> profil co N m -> NMT z ASC -> eksport XYZ.
#include <eu07/geo/puwg1992.hpp>
#include <eu07/nmt/asc_index.hpp>
#include <eu07/nmt/asc_lookup.hpp>
#include <eu07/nmt/asc_ram.hpp>
#include <eu07/nmt/parallel.hpp>
#include <eu07/nmt/profile_export.hpp>
#include <eu07/nmt/profile_types.hpp>
#include <eu07/nmt/route_geometry.hpp>
#include <eu07/nmt/terrain_cli.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/track_routes.hpp>
#include <filesystem>
#include <iostream>
#include <print>
#include <vector>
namespace eu07::nmt {
inline void runTerrainProfile(
const std::filesystem::path& stem,
const scene::SceneDocument& doc,
const scene::TrackRouteBuildResult& routes,
const geo::PuwgMasterOffset& master,
const std::filesystem::path& nmtDir,
const TerrainProfileOptions& options) {
const AscIndexResult loaded = loadOrBuildAscIndex(nmtDir);
const std::filesystem::path catalogRoot = loaded.index.catalogRoot;
std::println("\nNMT (ASC):");
std::println(" indeks: {} plikow ({})",
loaded.index.entries.size(),
loaded.status == AscIndexStatus::Created ? "utworzono"
: loaded.status == AscIndexStatus::Refreshed ? "odswiezono"
: "wczytano");
std::println(" profil: co {:.0f} m wzdloz osi toru", options.profileStepM);
std::println(" NMT: promien {:.1f} m od osi (najblizsza komorka ASC)", options.nmtRadiusM);
std::println(" watki: {}", workerThreadCount());
std::cerr << std::flush;
AscTileCache ascCache;
std::vector<RouteChainageSample> allProfileSamples;
allProfileSamples.reserve(routes.routes.size() * 512);
const std::size_t routeCount = routes.routes.size();
for (std::size_t routeIndex = 0; routeIndex < routeCount; ++routeIndex) {
const scene::TrackRoute& route = routes.routes[routeIndex];
std::cerr << "[NMT] trasa " << (routeIndex + 1) << '/' << routeCount << ": " << route.label
<< " — geometria toru...\n"
<< std::flush;
const RouteGeometry geometry = buildRouteGeometry(doc, routes, routeIndex);
if (geometry.segments.empty()) {
std::println(" trasa {}: {} — brak geometrii, pomijam", routeIndex + 1, route.label);
continue;
}
std::cerr << "[NMT] trasa " << (routeIndex + 1) << '/' << routeCount << " — profil co "
<< options.profileStepM << " m + NMT r=" << options.nmtRadiusM << " m...\n"
<< std::flush;
std::vector<RouteChainageSample> samples =
sampleRouteCenterlineFromGeometry(geometry, master, options.profileStepM);
for (RouteChainageSample& sample : samples) {
sample.routeIndex = static_cast<int>(routeIndex + 1);
}
const ProfileNmtResult nmt = processRouteProfileNmt(
loaded.index,
catalogRoot,
master,
options.nmtRadiusM,
samples,
ascCache);
allProfileSamples.insert(allProfileSamples.end(), samples.begin(), samples.end());
std::println(
" trasa {}: {} len={:.0f}m profil={} pkt (NMT {}) | cache {:.1f} MB",
routeIndex + 1,
route.label,
geometry.totalLengthM,
samples.size(),
nmt.samplesWithNmt,
static_cast<double>(ascCache.ramBytes) / (1024.0 * 1024.0));
}
if (!allProfileSamples.empty()) {
const std::filesystem::path xyzPath = stem.string() + ".profil10m.txt";
writeCombinedProfileXyz(xyzPath, allProfileSamples);
std::println(
" profil XYZ: {} ({} pkt, ASCII: east north height, EPSG:2180)",
xyzPath.string(),
allProfileSamples.size());
}
}
} // namespace eu07::nmt

View File

@@ -0,0 +1,500 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Plik_tekstowy
// + dyrektywy startera (//$…)
#include <array>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace eu07 {
struct SourceToken {
std::string value;
std::size_t sourceLine = 0; // indeks linii w pliku (0 = pierwsza)
};
// =============================================================================
// Tokeny (wiki)
// =============================================================================
[[nodiscard]] inline bool isFieldSeparator(const unsigned char ch) noexcept {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == ';' || ch == ',';
}
inline void skipFieldSeparators(std::string_view& text) noexcept {
while (!text.empty() && isFieldSeparator(static_cast<unsigned char>(text.front()))) {
text.remove_prefix(1);
}
}
inline void tokenizeInto(
std::string_view buffer,
std::vector<SourceToken>& out,
const std::size_t sourceLine = 0) {
const auto lineFor = [&](const char* /*tokenStart*/) -> std::size_t { return sourceLine; };
std::string_view text = buffer;
skipFieldSeparators(text);
while (!text.empty()) {
const char* const tokenStart = text.data();
if (text.front() == '"') {
text.remove_prefix(1);
std::string quoted;
while (!text.empty()) {
if (text.front() == '"') {
text.remove_prefix(1);
break;
}
if (text.front() == '\\') {
text.remove_prefix(1);
if (text.empty()) {
break;
}
if (text.front() == '\\') {
quoted.push_back('\\');
text.remove_prefix(1);
continue;
}
quoted.push_back(text.front());
text.remove_prefix(1);
continue;
}
quoted.push_back(text.front());
text.remove_prefix(1);
}
out.push_back(SourceToken{std::move(quoted), lineFor(tokenStart)});
skipFieldSeparators(text);
continue;
}
if (text.front() == '[') {
const std::size_t close = text.find(']', 1);
if (close != std::string_view::npos) {
out.push_back(SourceToken{
std::string(text.data(), close + 1),
lineFor(tokenStart),
});
text.remove_prefix(close + 1);
skipFieldSeparators(text);
continue;
}
}
const char* const start = text.data();
while (!text.empty() &&
!isFieldSeparator(static_cast<unsigned char>(text.front())) &&
text.front() != '"' &&
text.front() != '[') {
text.remove_prefix(1);
}
const std::size_t len = static_cast<std::size_t>(text.data() - start);
if (len > 0) {
out.push_back(SourceToken{std::string(start, len), lineFor(tokenStart)});
}
skipFieldSeparators(text);
}
}
[[nodiscard]] inline std::vector<std::string> tokenize(std::string_view text) {
std::vector<SourceToken> located;
located.reserve(8);
tokenizeInto(text, located);
std::vector<std::string> tokens;
tokens.reserve(located.size());
for (SourceToken& token : located) {
tokens.push_back(std::move(token.value));
}
return tokens;
}
// =============================================================================
// Dyrektywy startera (//$…)
// =============================================================================
enum class StarterId {
Name,
Description,
Link,
ConsistDesc,
Image,
ImageTrain,
DecorSkip,
Archive,
Category,
Error,
GeoMap,
Reference,
TerrainRegen,
ConsistParams,
ExeVersion,
};
struct StarterDirective {
StarterId id{};
std::size_t line = 0;
std::string value;
bool hiddenConsist = false;
};
namespace detail {
inline constexpr std::array<std::pair<std::string_view, StarterId>, 15> kStarterTags{{
{"//$decor", StarterId::DecorSkip},
{"//$it", StarterId::ImageTrain},
{"//$n", StarterId::Name},
{"//$d", StarterId::Description},
{"//$f", StarterId::Link},
{"//$o", StarterId::ConsistDesc},
{"//$i", StarterId::Image},
{"//$a", StarterId::Archive},
{"//$l", StarterId::Category},
{"//$e", StarterId::Error},
{"//$g", StarterId::GeoMap},
{"//$r", StarterId::Reference},
{"//$t", StarterId::TerrainRegen},
{"//$w", StarterId::ConsistParams},
{"//$x", StarterId::ExeVersion},
}};
[[nodiscard]] inline bool tagOk(const std::string_view line, const std::string_view tag) {
return line.size() == tag.size() ||
isFieldSeparator(static_cast<unsigned char>(line[tag.size()]));
}
[[nodiscard]] inline std::optional<StarterId> matchStarter(std::string_view line) {
skipFieldSeparators(line);
for (const auto& [tag, id] : kStarterTags) {
if (line.starts_with(tag) && tagOk(line, tag)) {
return id;
}
}
return std::nullopt;
}
[[nodiscard]] inline bool isStarter(std::string_view line) {
return matchStarter(line).has_value();
}
[[nodiscard]] inline bool isLineComment(std::string_view line) {
skipFieldSeparators(line);
return line.starts_with("//") && !isStarter(line);
}
[[nodiscard]] inline std::string_view stripInlineComment(std::string_view line) {
skipFieldSeparators(line);
if (line.starts_with("//$")) {
return {};
}
const std::size_t pos = line.find("//");
return pos == std::string_view::npos ? line : line.substr(0, pos);
}
} // namespace detail
[[nodiscard]] inline std::string_view starterTag(const StarterId id) {
for (const auto& [tag, sid] : detail::kStarterTags) {
if (sid == id) {
return tag;
}
}
return "//$?";
}
[[nodiscard]] inline std::array<std::string_view, 15> supportedStarterTags() {
std::array<std::string_view, 15> tags{};
for (std::size_t i = 0; i < detail::kStarterTags.size(); ++i) {
tags[i] = detail::kStarterTags[i].first;
}
return tags;
}
inline void writeStartersReport(
const std::filesystem::path& outPath,
const std::vector<StarterDirective>& starters) {
std::ofstream out(outPath);
if (!out) {
throw std::runtime_error("Nie mozna zapisac: " + outPath.string());
}
out << "# dyrektywy startera (//$…)\n";
out << "# obslugiwane:\n";
for (const std::string_view tag : supportedStarterTags()) {
out << "# " << tag << '\n';
}
out << "# wykryte: " << starters.size() << "\n\n";
for (const StarterDirective& s : starters) {
out << starterTag(s.id) << " L" << (s.line + 1) << ": \"" << s.value << '"';
if (s.hiddenConsist) {
out << " [ukryty]";
}
out << '\n';
}
}
[[nodiscard]] inline std::optional<StarterDirective> parseStarter(
std::string_view line,
const std::size_t lineNo) {
skipFieldSeparators(line);
const std::optional<StarterId> id = detail::matchStarter(line);
if (!id) {
return std::nullopt;
}
for (const auto& [tag, sid] : detail::kStarterTags) {
if (sid != *id) {
continue;
}
StarterDirective d;
d.id = *id;
d.line = lineNo;
std::string_view rest = line.substr(tag.size());
skipFieldSeparators(rest);
d.value.assign(rest);
if (*id == StarterId::ConsistDesc && !d.value.empty() && d.value.front() == '-') {
d.hiddenConsist = true;
std::string_view tail = d.value;
tail.remove_prefix(1);
skipFieldSeparators(tail);
d.value.assign(tail);
}
return d;
}
return std::nullopt;
}
// =============================================================================
// Komentarze: // … oraz /* … */ (wiki)
// =============================================================================
struct RawFile {
std::string data;
std::vector<std::string_view> lines;
};
[[nodiscard]] inline RawFile readRawFile(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("Nie mozna otworzyc: " + path.string());
}
RawFile f;
in.seekg(0, std::ios::end);
const std::streamoff fileSize = in.tellg();
in.seekg(0, std::ios::beg);
if (fileSize > 0) {
f.data.resize(static_cast<std::size_t>(fileSize));
in.read(f.data.data(), fileSize);
}
f.lines.reserve(f.data.empty() ? 0 : f.data.size() / 24 + 4);
std::size_t start = 0;
for (std::size_t i = 0; i <= f.data.size(); ++i) {
if (i == f.data.size() || f.data[i] == '\n') {
std::size_t end = i;
if (end > start && f.data[end - 1] == '\r') {
--end;
}
f.lines.emplace_back(f.data.data() + start, end - start);
start = i + 1;
}
}
return f;
}
struct LogicalSegment {
std::size_t sourceLine = 0;
std::string_view text;
};
struct LogicalPass {
std::vector<StarterDirective> starters;
std::vector<LogicalSegment> segments;
std::vector<std::string> spill;
};
[[nodiscard]] inline LogicalPass toLogicalLines(const std::vector<std::string_view>& raw) {
LogicalPass out;
out.segments.reserve(raw.size());
out.starters.reserve(8);
out.spill.reserve(4);
bool inBlock = false;
const auto emit = [&](std::string_view piece, const std::size_t lineNo) {
if (piece.empty()) {
return;
}
skipFieldSeparators(piece);
if (detail::isStarter(piece)) {
return;
}
if (detail::isLineComment(piece)) {
return;
}
const std::string_view code = detail::stripInlineComment(piece);
if (code.empty()) {
return;
}
LogicalSegment segment;
segment.sourceLine = lineNo;
if (code.data() >= piece.data() && code.data() + code.size() <= piece.data() + piece.size()) {
segment.text = code;
} else {
out.spill.emplace_back(code);
segment.text = out.spill.back();
}
out.segments.push_back(segment);
};
for (std::size_t n = 0; n < raw.size(); ++n) {
std::string_view line = raw[n];
{
std::string_view whole = line;
skipFieldSeparators(whole);
if (const std::optional<StarterDirective> starter = parseStarter(whole, n)) {
out.starters.push_back(*starter);
continue;
}
}
if (!inBlock) {
std::size_t at = 0;
while (at < line.size()) {
const std::size_t slash = line.find('/', at);
if (slash == std::string_view::npos) {
emit(line.substr(at), n);
break;
}
if (slash + 1 < line.size() && line[slash + 1] == '/') {
if (slash > at) {
emit(line.substr(at, slash - at), n);
}
break;
}
if (slash + 1 < line.size() && line[slash + 1] == '*') {
if (slash > at) {
emit(line.substr(at, slash - at), n);
}
inBlock = true;
line = line.substr(slash + 2);
at = 0;
continue;
}
// Pojedynczy '/' to separator sciezki (np. 6-9-9/l33230.inc), nie komentarz.
emit(line.substr(at), n);
break;
}
continue;
}
while (inBlock && !line.empty()) {
const std::size_t close = line.find("*/");
if (close == std::string_view::npos) {
line = {};
break;
}
line.remove_prefix(close + 2);
inBlock = false;
}
if (!inBlock && !line.empty()) {
emit(line, n);
}
}
return out;
}
[[nodiscard]] inline std::vector<StarterDirective> scanStarters(
const std::vector<std::string_view>& rawLines) {
return toLogicalLines(rawLines).starters;
}
[[nodiscard]] inline bool isBracketToken(std::string_view token) {
skipFieldSeparators(token);
return token.size() >= 2 && token.front() == '[' && token.back() == ']';
}
struct ParseOptions {
bool tokenize = true;
};
struct ParseResult {
std::vector<StarterDirective> starters;
std::vector<SourceToken> tokens;
};
[[nodiscard]] inline std::vector<SourceToken> tokenizeSegments(
const std::vector<LogicalSegment>& segments) {
std::vector<SourceToken> tokens;
if (segments.empty()) {
return tokens;
}
std::size_t chars = 0;
for (const LogicalSegment& segment : segments) {
chars += segment.text.size();
}
tokens.reserve(chars / 10 + 16);
for (const LogicalSegment& segment : segments) {
tokenizeInto(segment.text, tokens, segment.sourceLine);
}
return tokens;
}
[[nodiscard]] inline ParseResult parseFile(
const std::filesystem::path& path,
const ParseOptions& options = {}) {
const RawFile raw = readRawFile(path);
LogicalPass logical = toLogicalLines(raw.lines);
ParseResult result;
result.starters = std::move(logical.starters);
if (options.tokenize) {
result.tokens = tokenizeSegments(logical.segments);
}
return result;
}
[[nodiscard]] inline ParseResult parseText(const std::string& text) {
RawFile raw;
raw.data = text;
std::size_t start = 0;
for (std::size_t i = 0; i <= raw.data.size(); ++i) {
if (i == raw.data.size() || raw.data[i] == '\n') {
std::size_t end = i;
if (end > start && raw.data[end - 1] == '\r') {
--end;
}
raw.lines.emplace_back(raw.data.data() + start, end - start);
start = i + 1;
}
}
LogicalPass logical = toLogicalLines(raw.lines);
ParseResult result;
result.starters = std::move(logical.starters);
result.tokens = tokenizeSegments(logical.segments);
return result;
}
} // namespace eu07

View File

@@ -0,0 +1,46 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_area
// area name tracks endarea
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/boundary.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::area {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "area")) {
return false;
}
const std::size_t anchor = stream.checkpoint();
DirectiveBlock block;
block.line = stream.consume().sourceLine;
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endarea")) {
session.take();
ctx.document.area.push_back(std::move(block));
return true;
}
const std::string_view value = session.peek().value;
if (detail::isTopLevelStarter(value) || detail::isEmbeddedInNode(value)) {
session.fail();
stream.rewind(anchor);
return false;
}
session.take();
}
session.fail();
stream.rewind(anchor);
return false;
}
} // namespace eu07::scene::area

View File

@@ -0,0 +1,35 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_atmo
// atmo skyColor fogRangeStart fogRangeEnd fogColor overcast endatmo
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::atmo {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "atmo")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endatmo")) {
session.take();
break;
}
session.take();
}
ctx.document.atmo.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::atmo

View File

@@ -0,0 +1,807 @@
#pragma once
// Rekurencyjny bake: kazdy plik tekstowy → wlasny .eu7, dzieci z INCL tez.
// Znaleziony include → od razu na kolejke i wolny watek (bez osobnej fazy odkrywania).
#include <eu07/parser.hpp>
#include <eu07/scene/bake/compose_pack_models.hpp>
#include <eu07/scene/bake/module.hpp>
#include <eu07/scene/binary/runtime_module.hpp>
#include <eu07/scene/include_resolve.hpp>
#include <eu07/scene/include_scan.hpp>
#include <eu07/scene/processor.hpp>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <exception>
#include <filesystem>
#include <functional>
#include <iostream>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>
namespace eu07::scene::bake {
struct BakeTreeStats {
std::size_t modulesBaked = 0;
std::vector<std::filesystem::path> writtenPaths;
bool has_pack_diagnostics = false;
PackComposeStatsSnapshot pack_compose;
binary::PackWriteStats pack_write;
};
enum class BakeProgressPhase : std::uint8_t {
Starting,
Start,
PackModels,
PackCompose,
PackComposeDone,
Bake,
Done,
};
struct BakeProgress {
BakeProgressPhase phase = BakeProgressPhase::Bake;
std::size_t current = 0;
std::size_t total = 0;
std::filesystem::path path;
unsigned threads = 1;
const PackComposeStats* pack_stats = nullptr;
};
using BakeProgressCallback = std::function<void(const BakeProgress&)>;
struct BakeTreeOptions {
unsigned maxThreads = 0;
BakeProgressCallback onProgress;
};
struct BakeTreeContext {
std::filesystem::path sceneryRoot;
std::string currentRelativeFile;
};
namespace detail {
struct BakeWorkItem {
std::filesystem::path path;
std::string relativeFile;
std::vector<std::filesystem::path> includeStack;
};
struct BakePoolState {
std::mutex queueMutex;
std::condition_variable queueCv;
std::deque<BakeWorkItem> queue;
std::mutex seenMutex;
std::unordered_set<std::string> seen;
std::atomic<std::size_t> inFlight{0};
std::atomic<std::size_t> completed{0};
std::atomic<bool> stop{false};
std::mutex statsMutex;
std::mutex rootMutex;
std::mutex progressMutex;
std::mutex errorMutex;
std::exception_ptr firstError;
};
[[nodiscard]] inline BakeTreeContext makeBakeTreeContext(const std::filesystem::path& inputPath) {
const std::filesystem::path resolved = inputPath.lexically_normal();
BakeTreeContext context;
if (const std::optional<std::filesystem::path> sceneryRoot =
eu07::scene::detail::findSceneryRootInPath(resolved)) {
context.sceneryRoot = *sceneryRoot;
context.currentRelativeFile =
eu07::scene::detail::relativeSceneryFile(context.sceneryRoot, resolved);
} else {
context.sceneryRoot = resolved.parent_path();
context.currentRelativeFile = resolved.filename().generic_string();
}
return context;
}
[[nodiscard]] inline std::string canonicalPathKey(const std::filesystem::path& path) {
return path.lexically_normal().generic_string();
}
[[nodiscard]] inline unsigned defaultPoolThreadCount() {
const unsigned hw =
std::thread::hardware_concurrency() == 0 ? 4u : std::thread::hardware_concurrency();
return std::max(1u, hw);
}
[[nodiscard]] inline unsigned resolvePoolThreadCount(const unsigned maxThreads) {
if (maxThreads == 0) {
return defaultPoolThreadCount();
}
return std::max(1u, maxThreads);
}
inline void recordError(BakePoolState& pool) {
std::lock_guard lock(pool.errorMutex);
if (!pool.firstError) {
pool.firstError = std::current_exception();
}
pool.stop.store(true, std::memory_order_release);
pool.queueCv.notify_all();
}
inline void enqueueWork(BakePoolState& pool, BakeWorkItem item) {
const std::string key = canonicalPathKey(item.path);
{
std::lock_guard lock(pool.seenMutex);
if (!pool.seen.insert(key).second) {
return;
}
}
{
std::lock_guard lock(pool.queueMutex);
pool.queue.push_back(std::move(item));
}
pool.queueCv.notify_one();
}
inline void bakeWorkItem(
const BakeWorkItem& item,
const BakeTreeContext& context,
const std::string& rootKey,
BakePoolState& pool,
BakeTreeStats* stats,
RuntimeModule& rootModuleOut,
const BakeTreeOptions& options,
const unsigned threadCount) {
if (pool.stop.load(std::memory_order_acquire)) {
return;
}
if (eu07::scene::detail::isIncludeCycle(item.path, item.includeStack)) {
throw std::runtime_error("EU7 bake: cykliczny include: " + item.path.string());
}
if (!std::filesystem::exists(item.path)) {
if (item.includeStack.empty()) {
throw std::runtime_error("EU7 bake: brak pliku: " + item.path.string());
}
return;
}
pool.inFlight.fetch_add(1, std::memory_order_relaxed);
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::Start,
pool.completed.load(std::memory_order_relaxed),
0,
item.path,
threadCount,
});
}
try {
const ParseResult parsed = parseFile(item.path);
SceneProcessOptions shallowOpts;
shallowOpts.expandIncludes = false;
const SceneDocument document =
processScene(parsed, context.sceneryRoot, shallowOpts).document;
const bool emitPackModels = canonicalPathKey(item.path) == rootKey;
RuntimeModule module = bakeModule(document, {.skipLocalModels = emitPackModels});
const std::filesystem::path eu7Path =
item.path.parent_path() / (item.path.stem().string() + ".eu7");
const std::string key = canonicalPathKey(item.path);
binary::WriteRuntimeModuleOptions writeOpts;
writeOpts.emitPackModels = emitPackModels;
std::vector<binary::codec::ModelSectionBatch> packBatches;
if (writeOpts.emitPackModels) {
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::PackModels,
pool.completed.load(std::memory_order_relaxed),
0,
item.path,
threadCount,
});
}
PackComposeStats pack_stats;
PackComposeProgress pack_progress;
if (options.onProgress) {
pack_progress.on_progress =
[&](const std::filesystem::path& file,
const std::size_t files_visited,
const std::size_t models_total) {
std::lock_guard lock(pool.progressMutex);
BakeProgress progress{
BakeProgressPhase::PackCompose,
files_visited,
models_total,
file,
threadCount,
};
progress.pack_stats = &pack_stats;
options.onProgress(progress);
};
}
PackComposeOptions pack_options;
pack_options.progress = &pack_progress;
pack_options.stats = &pack_stats;
pack_options.max_threads = threadCount;
pack_options.pack_batches = &packBatches;
PackComposeSeed root_seed;
root_seed.path = item.path.lexically_normal();
root_seed.entry = makePackFileCacheEntry(document);
pack_options.root_seed = &root_seed;
if (options.onProgress) {
std::cerr << "[EU7] PACK compose: " << threadCount << " watkow\n" << std::flush;
}
const std::size_t pack_model_count = composePackModels(
item.path, context.sceneryRoot, item.relativeFile, pack_options);
writeOpts.pack_batches = &packBatches;
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::PackComposeDone,
pack_progress.files_visited,
pack_model_count,
item.path,
threadCount,
});
std::cerr << "[EU7] PACK gotowe: modele=" << pack_model_count
<< " sekcji=" << packBatches.size() << '\n'
<< std::flush;
}
printPackComposeStats(pack_stats, std::cerr);
if (stats != nullptr) {
stats->has_pack_diagnostics = true;
stats->pack_compose = snapshotPackComposeStats(pack_stats);
}
}
binary::PackWriteStats pack_write_stats;
if (writeOpts.emitPackModels) {
writeOpts.pack_write_stats = &pack_write_stats;
std::cerr << "[EU7] zapis .eu7 (PACK)...\n" << std::flush;
}
binary::writeRuntimeModule(eu7Path, module, writeOpts);
if (writeOpts.emitPackModels) {
printPackWriteStats(pack_write_stats, std::cerr);
if (stats != nullptr) {
stats->pack_write = pack_write_stats;
}
}
const std::size_t done = pool.completed.fetch_add(1, std::memory_order_relaxed) + 1;
if (stats != nullptr) {
std::lock_guard lock(pool.statsMutex);
++stats->modulesBaked;
stats->writtenPaths.push_back(eu7Path);
}
if (key == rootKey) {
std::lock_guard lock(pool.rootMutex);
rootModuleOut = std::move(module);
}
if (options.onProgress) {
std::lock_guard lock(pool.progressMutex);
options.onProgress(
BakeProgress{
BakeProgressPhase::Bake,
done,
0,
item.path,
threadCount,
});
}
BakeWorkItem child;
child.includeStack = item.includeStack;
child.includeStack.push_back(item.path);
for (const std::string& includeFile : scanIncludeFilePaths(parsed.tokens)) {
child.path = eu07::scene::detail::resolveIncludeSourcePath(
context.sceneryRoot, item.relativeFile, includeFile);
child.relativeFile = eu07::scene::detail::relativeSceneryFile(
context.sceneryRoot, child.path);
enqueueWork(pool, child);
}
} catch (...) {
recordError(pool);
pool.inFlight.fetch_sub(1, std::memory_order_relaxed);
pool.queueCv.notify_all();
return;
}
pool.inFlight.fetch_sub(1, std::memory_order_relaxed);
pool.queueCv.notify_all();
}
inline void bakePoolWorker(
BakePoolState& pool,
const BakeTreeContext& context,
const std::string& rootKey,
BakeTreeStats* stats,
RuntimeModule& rootModuleOut,
const BakeTreeOptions& options,
const unsigned threadCount) {
while (true) {
BakeWorkItem item;
{
std::unique_lock lock(pool.queueMutex);
pool.queueCv.wait(lock, [&]() {
return pool.stop.load(std::memory_order_acquire) || !pool.queue.empty() ||
pool.inFlight.load(std::memory_order_relaxed) == 0;
});
if (pool.stop.load(std::memory_order_acquire)) {
return;
}
if (pool.queue.empty()) {
if (pool.inFlight.load(std::memory_order_relaxed) == 0) {
return;
}
continue;
}
item = std::move(pool.queue.front());
pool.queue.pop_front();
}
try {
bakeWorkItem(
item, context, rootKey, pool, stats, rootModuleOut, options, threadCount);
} catch (...) {
recordError(pool);
}
if (pool.stop.load(std::memory_order_acquire)) {
return;
}
}
}
inline void runBakePool(
const std::filesystem::path& rootSourcePath,
const BakeTreeContext& context,
BakeTreeStats* stats,
RuntimeModule& rootModuleOut,
const BakeTreeOptions& options) {
const std::string rootKey = canonicalPathKey(rootSourcePath.lexically_normal());
const unsigned threadCount = resolvePoolThreadCount(options.maxThreads);
if (options.onProgress) {
options.onProgress(
BakeProgress{
BakeProgressPhase::Starting,
0,
0,
rootSourcePath,
threadCount,
});
}
BakePoolState pool;
enqueueWork(
pool,
BakeWorkItem{
rootSourcePath.lexically_normal(),
context.currentRelativeFile,
{},
});
std::vector<std::thread> workers;
workers.reserve(threadCount);
for (unsigned i = 0; i < threadCount; ++i) {
workers.emplace_back(
bakePoolWorker,
std::ref(pool),
std::cref(context),
std::cref(rootKey),
stats,
std::ref(rootModuleOut),
std::cref(options),
threadCount);
}
for (std::thread& worker : workers) {
worker.join();
}
if (pool.firstError) {
std::rethrow_exception(pool.firstError);
}
if (options.onProgress) {
const std::size_t done = pool.completed.load(std::memory_order_relaxed);
options.onProgress(
BakeProgress{
BakeProgressPhase::Done,
done,
done,
rootSourcePath,
threadCount,
});
}
}
} // namespace detail
[[nodiscard]] inline RuntimeModule bakeModuleTree(
const std::filesystem::path& inputPath,
BakeTreeStats* stats = nullptr,
const BakeTreeOptions& options = {}) {
const BakeTreeContext context = detail::makeBakeTreeContext(inputPath);
RuntimeModule rootModule;
detail::runBakePool(inputPath, context, stats, rootModule, options);
return rootModule;
}
} // namespace eu07::scene::bake

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
#pragma once
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/dynamic.hpp>
#include <eu07/scene/runtime/nodes.hpp>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeDynamicObject bakeDynamic(const ParsedNodeDynamic& parsed) {
runtime::RuntimeDynamicObject vehicle;
vehicle.node = bakeBasicNode(parsed.header, "dynamic");
vehicle.dataFolder = parsed.datafolder;
vehicle.skinFile = parsed.skinfile;
vehicle.mmdFile = parsed.mmdfile;
vehicle.trackName = parsed.pathname;
vehicle.offset = parsed.vehicleOffset;
vehicle.driverType = parsed.driverType;
vehicle.coupling = parsed.coupling.type;
vehicle.couplingRaw = parsed.coupling.raw;
vehicle.couplingParams = parsed.coupling.params;
vehicle.velocity = static_cast<float>(parsed.velocity);
vehicle.loadCount = parsed.loadcount;
if (parsed.loadtype) {
vehicle.loadType = *parsed.loadtype;
}
vehicle.destination = parsed.destination;
if (parsed.header.trainsetIndex) {
vehicle.trainsetIndex = *parsed.header.trainsetIndex;
}
return vehicle;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,164 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/runtime/directives.hpp>
#include <cctype>
#include <string>
#include <vector>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::EventType parseEventType(std::string_view type) {
if (isKeywordIgnoreCase(type, "addvalues")) {
return runtime::EventType::AddValues;
}
if (isKeywordIgnoreCase(type, "updatevalues")) {
return runtime::EventType::UpdateValues;
}
if (isKeywordIgnoreCase(type, "copyvalues")) {
return runtime::EventType::CopyValues;
}
if (isKeywordIgnoreCase(type, "getvalues")) {
return runtime::EventType::GetValues;
}
if (isKeywordIgnoreCase(type, "putvalues")) {
return runtime::EventType::PutValues;
}
if (isKeywordIgnoreCase(type, "whois")) {
return runtime::EventType::Whois;
}
if (isKeywordIgnoreCase(type, "logvalues")) {
return runtime::EventType::LogValues;
}
if (isKeywordIgnoreCase(type, "multiple")) {
return runtime::EventType::Multiple;
}
if (isKeywordIgnoreCase(type, "switch")) {
return runtime::EventType::Switch;
}
if (isKeywordIgnoreCase(type, "trackvel")) {
return runtime::EventType::TrackVel;
}
if (isKeywordIgnoreCase(type, "sound")) {
return runtime::EventType::Sound;
}
if (isKeywordIgnoreCase(type, "texture")) {
return runtime::EventType::Texture;
}
if (isKeywordIgnoreCase(type, "animation")) {
return runtime::EventType::Animation;
}
if (isKeywordIgnoreCase(type, "lights")) {
return runtime::EventType::Lights;
}
if (isKeywordIgnoreCase(type, "voltage")) {
return runtime::EventType::Voltage;
}
if (isKeywordIgnoreCase(type, "visible")) {
return runtime::EventType::Visible;
}
if (isKeywordIgnoreCase(type, "friction")) {
return runtime::EventType::Friction;
}
if (isKeywordIgnoreCase(type, "message")) {
return runtime::EventType::Message;
}
return runtime::EventType::Unknown;
}
namespace detail {
inline void splitTargetList(const std::string& text, std::vector<std::string>& out) {
std::string current;
for (const char ch : text) {
if (ch == '|' || ch == ',') {
if (!current.empty() && !isKeywordIgnoreCase(current, "none")) {
out.push_back(current);
}
current.clear();
} else if (!std::isspace(static_cast<unsigned char>(ch))) {
current.push_back(ch);
}
}
if (!current.empty() && !isKeywordIgnoreCase(current, "none")) {
out.push_back(current);
}
}
} // namespace detail
[[nodiscard]] inline runtime::RuntimeEvent bakeEvent(const DirectiveBlock& block) {
runtime::RuntimeEvent event;
TokenStream stream(block.tokens);
if (stream.empty() || !isKeyword(stream.peek().value, "event")) {
return event;
}
stream.consume();
std::vector<SourceToken> raw;
std::string name;
std::string type;
double delay = 0.0;
if (!node::io::takeString(stream, raw, name) || !node::io::takeString(stream, raw, type) ||
!node::io::takeDouble(stream, raw, delay)) {
return event;
}
event.name = std::move(name);
event.type = parseEventType(type);
event.delay = delay;
if (event.name.starts_with("none_")) {
event.ignored = true;
}
if (!stream.empty() && !isKeyword(stream.peek().value, "endevent")) {
std::string targets;
if (node::io::takeString(stream, raw, targets)) {
detail::splitTargetList(targets, event.targets);
}
}
while (!stream.empty() && !isKeyword(stream.peek().value, "endevent")) {
std::string token;
if (!node::io::takeString(stream, raw, token)) {
break;
}
if (isKeywordIgnoreCase(token, "randomdelay")) {
double value = 0.0;
if (node::io::takeDouble(stream, raw, value)) {
event.delayRandom = value;
}
continue;
}
if (isKeywordIgnoreCase(token, "departuredelay")) {
double value = 0.0;
if (node::io::takeDouble(stream, raw, value)) {
event.delayDeparture = value;
}
continue;
}
if (isKeywordIgnoreCase(token, "passive")) {
event.passive = true;
continue;
}
event.payload.emplace_back(std::move(token), "");
}
return event;
}
[[nodiscard]] inline std::vector<runtime::RuntimeEvent> bakeEvents(const SceneDocument& document) {
std::vector<runtime::RuntimeEvent> events;
events.reserve(document.event.size());
for (const DirectiveBlock& block : document.event) {
events.push_back(bakeEvent(block));
}
return events;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,59 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/eventlauncher.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <cmath>
namespace eu07::scene::bake {
[[nodiscard]] inline double parseLauncherTime(const std::string& token) {
try {
return std::stod(token);
} catch (...) {
return -1.0;
}
}
[[nodiscard]] inline runtime::RuntimeEventLauncher bakeEventlauncher(const ParsedNodeEventlauncher& parsed) {
runtime::RuntimeEventLauncher launcher;
launcher.node = bakeBasicNode(parsed.header, "eventlauncher");
const runtime::RuntimeScratchpad scratch = scratchFromHeader(parsed.header);
launcher.location = runtime::transformPoint(parsed.position, scratch);
launcher.node.area.center = launcher.location;
if (parsed.radius > 0.0) {
launcher.radiusSquared = parsed.radius * parsed.radius;
}
launcher.activationKeyRaw = parsed.key;
launcher.event1Name = parsed.event1;
launcher.event2Name = parsed.event2.value_or("none");
double delta = parseLauncherTime(parsed.time);
if (delta > 0.0) {
launcher.launchMinute = static_cast<int>(delta) % 100;
launcher.launchHour = static_cast<int>(delta - launcher.launchMinute) / 100;
launcher.deltaTime = 0.0;
} else {
launcher.deltaTime = delta < 0.0 ? -delta : delta;
}
if (parsed.condition) {
runtime::EventLauncherCondition cond;
cond.memcellName = parsed.condition->memcell;
cond.compareText = parsed.condition->parameter;
cond.compareValue1 = parsed.condition->value1.value_or(0.0);
cond.compareValue2 = parsed.condition->value2.value_or(0.0);
cond.checkMask = parsed.condition->checkMask;
launcher.condition = cond;
}
launcher.trainTriggered = parsed.trainTriggered;
return launcher;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,188 @@
#pragma once
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/types.hpp>
#include <eu07/scene/runtime/transform.hpp>
#include <eu07/scene/runtime/types.hpp>
#include <cmath>
#include <vector>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeScratchpad scratchFromHeader(const NodeHeader& header) {
runtime::RuntimeScratchpad scratch;
scratch.location.offsetStack.push_back(header.originOffset);
scratch.location.rotation = header.rotation;
scratch.location.scaleStack.push_back(header.scaleFactor);
return scratch;
}
[[nodiscard]] inline runtime::WorldVertex meshVertexToWorld(const MeshVertex& v) {
runtime::WorldVertex out;
out.position = {v.x, v.y, v.z};
out.normal = {v.nx, v.ny, v.nz};
out.u = v.u;
out.v = v.v;
return out;
}
[[nodiscard]] inline bool degenerateTriangle(
const runtime::Vec3& a,
const runtime::Vec3& b,
const runtime::Vec3& c) {
const runtime::Vec3 ab{b.x - a.x, b.y - a.y, b.z - a.z};
const runtime::Vec3 ac{c.x - a.x, c.y - a.y, c.z - a.z};
const runtime::Vec3 cross{
ab.y * ac.z - ab.z * ac.y,
ab.z * ac.x - ab.x * ac.z,
ab.x * ac.y - ab.y * ac.x};
const double len2 = cross.x * cross.x + cross.y * cross.y + cross.z * cross.z;
return len2 < 1e-12;
}
inline void pushTriangle(
std::vector<runtime::WorldVertex>& out,
const runtime::WorldVertex& v0,
const runtime::WorldVertex& v1,
const runtime::WorldVertex& v2) {
if (degenerateTriangle(v0.position, v1.position, v2.position)) {
return;
}
out.push_back(v0);
out.push_back(v1);
out.push_back(v2);
}
[[nodiscard]] inline std::vector<runtime::WorldVertex> triangulateTriangles(
const std::vector<MeshVertex>& source) {
std::vector<runtime::WorldVertex> out;
out.reserve(source.size());
for (std::size_t i = 0; i + 2 < source.size(); i += 3) {
pushTriangle(
out,
meshVertexToWorld(source[i]),
meshVertexToWorld(source[i + 1]),
meshVertexToWorld(source[i + 2]));
}
return out;
}
[[nodiscard]] inline std::vector<runtime::WorldVertex> triangulateTriangleFan(
const std::vector<MeshVertex>& source) {
std::vector<runtime::WorldVertex> out;
if (source.size() < 3) {
return out;
}
const runtime::WorldVertex v0 = meshVertexToWorld(source[0]);
runtime::WorldVertex v1 = meshVertexToWorld(source[1]);
for (std::size_t i = 2; i < source.size(); ++i) {
const runtime::WorldVertex v2 = meshVertexToWorld(source[i]);
pushTriangle(out, v0, v1, v2);
v1 = v2;
}
return out;
}
[[nodiscard]] inline std::vector<runtime::WorldVertex> triangulateTriangleStrip(
const std::vector<MeshVertex>& source) {
std::vector<runtime::WorldVertex> out;
if (source.size() < 3) {
return out;
}
runtime::WorldVertex v0 = meshVertexToWorld(source[0]);
runtime::WorldVertex v1 = meshVertexToWorld(source[1]);
for (std::size_t i = 2; i < source.size(); ++i) {
const runtime::WorldVertex v2 = meshVertexToWorld(source[i]);
if (i % 2 == 0) {
pushTriangle(out, v0, v1, v2);
} else {
pushTriangle(out, v1, v0, v2);
}
v0 = v1;
v1 = v2;
}
return out;
}
inline void computeBounds(runtime::RuntimeShapeNode& shape) {
if (shape.vertices.empty()) {
return;
}
runtime::Vec3 center{};
for (const runtime::WorldVertex& v : shape.vertices) {
center.x += v.position.x;
center.y += v.position.y;
center.z += v.position.z;
}
const double inv = 1.0 / static_cast<double>(shape.vertices.size());
center.x *= inv;
center.y *= inv;
center.z *= inv;
shape.node.area.center = center;
double radiusSq = 0.0;
for (const runtime::WorldVertex& v : shape.vertices) {
const double dx = v.position.x - center.x;
const double dy = v.position.y - center.y;
const double dz = v.position.z - center.z;
radiusSq = std::max(radiusSq, dx * dx + dy * dy + dz * dz);
}
shape.node.area.radius = static_cast<float>(std::sqrt(radiusSq));
}
inline void computeLineBounds(runtime::RuntimeLinesNode& lines) {
if (lines.vertices.empty()) {
return;
}
runtime::Vec3 center{};
for (const runtime::WorldVertex& v : lines.vertices) {
center.x += v.position.x;
center.y += v.position.y;
center.z += v.position.z;
}
const double inv = 1.0 / static_cast<double>(lines.vertices.size());
center.x *= inv;
center.y *= inv;
center.z *= inv;
lines.node.area.center = center;
double radiusSq = 0.0;
for (const runtime::WorldVertex& v : lines.vertices) {
const double dx = v.position.x - center.x;
const double dy = v.position.y - center.y;
const double dz = v.position.z - center.z;
radiusSq = std::max(radiusSq, dx * dx + dy * dy + dz * dz);
}
lines.node.area.radius = static_cast<float>(std::sqrt(radiusSq));
}
[[nodiscard]] inline runtime::LightingData bakeLighting(const NodeMaterial& material) {
runtime::LightingData lit;
lit.ambient = {
static_cast<float>(material.ambient.r / 255.0),
static_cast<float>(material.ambient.g / 255.0),
static_cast<float>(material.ambient.b / 255.0),
1.f};
lit.diffuse = {
static_cast<float>(material.diffuse.r / 255.0),
static_cast<float>(material.diffuse.g / 255.0),
static_cast<float>(material.diffuse.b / 255.0),
1.f};
lit.specular = {
static_cast<float>(material.specular.r / 255.0),
static_cast<float>(material.specular.g / 255.0),
static_cast<float>(material.specular.b / 255.0),
1.f};
return lit;
}
[[nodiscard]] inline runtime::LightingData defaultShapeLighting() {
return runtime::LightingData{};
}
[[nodiscard]] inline bool guessTranslucent(const std::string& texturePath) {
return texturePath.find('@') != std::string::npos;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,82 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/line_loop.hpp>
#include <eu07/scene/node/line_strip.hpp>
#include <eu07/scene/node/lines.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <algorithm>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeLinesNode bakeLinesNode(
const NodeHeader& header,
const std::string& nodeType,
const MaterialRgb& color,
const double thickness,
std::vector<runtime::WorldVertex> vertices) {
runtime::RuntimeLinesNode lines;
lines.node = bakeBasicNode(header, nodeType);
lines.lineWidth = static_cast<float>(std::min(30.0, thickness));
lines.lighting.diffuse = {
static_cast<float>(color.r / 255.0),
static_cast<float>(color.g / 255.0),
static_cast<float>(color.b / 255.0),
1.f};
lines.origin = {};
runtime::RuntimeScratchpad scratch;
scratch.location.offsetStack.push_back(header.originOffset);
scratch.location.rotation = header.rotation;
runtime::transformShapeVertices(vertices, scratch);
lines.vertices = std::move(vertices);
computeLineBounds(lines);
return lines;
}
[[nodiscard]] inline runtime::WorldVertex pointVertex(const Vec3& p) {
runtime::WorldVertex v;
v.position = p;
return v;
}
[[nodiscard]] inline runtime::RuntimeLinesNode bakeLines(const ParsedNodeLines& parsed) {
std::vector<runtime::WorldVertex> vertices;
vertices.reserve(parsed.segments.size() * 2);
for (const LineSegment& seg : parsed.segments) {
vertices.push_back(pointVertex(seg.a));
vertices.push_back(pointVertex(seg.b));
}
return bakeLinesNode(parsed.header, "lines", parsed.color, parsed.thickness, std::move(vertices));
}
[[nodiscard]] inline runtime::RuntimeLinesNode bakeLineStrip(const ParsedNodeLineStrip& parsed) {
std::vector<runtime::WorldVertex> vertices;
if (parsed.points.size() > 1) {
vertices.reserve((parsed.points.size() - 1) * 2);
for (std::size_t i = 1; i < parsed.points.size(); ++i) {
vertices.push_back(pointVertex(parsed.points[i - 1]));
vertices.push_back(pointVertex(parsed.points[i]));
}
}
return bakeLinesNode(parsed.header, "line_strip", parsed.color, parsed.thickness, std::move(vertices));
}
[[nodiscard]] inline runtime::RuntimeLinesNode bakeLineLoop(const ParsedNodeLineLoop& parsed) {
std::vector<runtime::WorldVertex> vertices;
if (parsed.points.size() > 1) {
vertices.reserve(parsed.points.size() * 2);
for (std::size_t i = 1; i < parsed.points.size(); ++i) {
vertices.push_back(pointVertex(parsed.points[i - 1]));
vertices.push_back(pointVertex(parsed.points[i]));
}
vertices.push_back(pointVertex(parsed.points.back()));
vertices.push_back(pointVertex(parsed.points.front()));
}
return bakeLinesNode(parsed.header, "line_loop", parsed.color, parsed.thickness, std::move(vertices));
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,24 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/memcell.hpp>
#include <eu07/scene/runtime/nodes.hpp>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeMemCell bakeMemcell(const ParsedNodeMemcell& parsed) {
runtime::RuntimeMemCell cell;
cell.node = bakeBasicNode(parsed.header, "memcell");
cell.text = parsed.command;
cell.value1 = parsed.value1;
cell.value2 = parsed.value2;
cell.trackName = parsed.trackName;
const runtime::RuntimeScratchpad scratch = scratchFromHeader(parsed.header);
cell.node.area.center = runtime::transformPoint(parsed.position, scratch);
return cell;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,69 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/triangle_fan.hpp>
#include <eu07/scene/node/triangle_strip.hpp>
#include <eu07/scene/node/triangles.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <optional>
#include <string>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeShapeNode bakeShape(
const NodeHeader& header,
const std::string& nodeType,
const std::string& texturePath,
const std::optional<NodeMaterial>& material,
std::vector<runtime::WorldVertex> vertices) {
runtime::RuntimeShapeNode shape;
shape.node = bakeBasicNode(header, nodeType);
shape.materialPath = texturePath;
shape.translucent = guessTranslucent(texturePath);
shape.lighting = material ? bakeLighting(*material) : defaultShapeLighting();
shape.origin = {};
runtime::RuntimeScratchpad scratch;
scratch.location.offsetStack.push_back(header.originOffset);
scratch.location.rotation = header.rotation;
runtime::transformShapeVertices(vertices, scratch);
shape.vertices = std::move(vertices);
computeBounds(shape);
return shape;
}
[[nodiscard]] inline runtime::RuntimeShapeNode bakeTriangles(const ParsedNodeTriangles& parsed) {
const bool hasMaterial =
parsed.material.ambient.r != 0.0 || parsed.material.ambient.g != 0.0 ||
parsed.material.ambient.b != 0.0 || parsed.material.diffuse.r != 0.0 ||
parsed.material.diffuse.g != 0.0 || parsed.material.diffuse.b != 0.0;
return bakeShape(
parsed.header,
"triangles",
parsed.texture,
hasMaterial ? std::optional<NodeMaterial>{parsed.material} : std::nullopt,
triangulateTriangles(parsed.vertices));
}
[[nodiscard]] inline runtime::RuntimeShapeNode bakeTriangleStrip(const ParsedNodeTriangleStrip& parsed) {
return bakeShape(
parsed.header,
"triangle_strip",
parsed.texture,
std::nullopt,
triangulateTriangleStrip(parsed.vertices));
}
[[nodiscard]] inline runtime::RuntimeShapeNode bakeTriangleFan(const ParsedNodeTriangleFan& parsed) {
return bakeShape(
parsed.header,
"triangle_fan",
parsed.texture,
std::nullopt,
triangulateTriangleFan(parsed.vertices));
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,64 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/model.hpp>
#include <eu07/scene/runtime/nodes.hpp>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeModelInstance bakeModel(const ParsedNodeModel& parsed) {
runtime::RuntimeModelInstance model;
model.node = bakeBasicNode(parsed.header, "model");
model.isTerrain = parsed.header.rangeMin < 0.0;
const runtime::RuntimeScratchpad scratch = scratchFromHeader(parsed.header);
model.location = runtime::transformPoint(parsed.location, scratch);
model.angles = parsed.header.rotation;
model.angles.y += parsed.rotationY;
if (parsed.angles) {
model.angles.x += parsed.angles->x;
model.angles.y += parsed.angles->y;
model.angles.z += parsed.angles->z;
}
model.scale = parsed.header.scaleFactor;
if (parsed.inlineScale) {
model.scale.x *= parsed.inlineScale->x;
model.scale.y *= parsed.inlineScale->y;
model.scale.z *= parsed.inlineScale->z;
}
model.modelFile = parsed.modelPath;
model.textureFile = parsed.replaceableSkin;
model.transition = !parsed.noTransition;
if (parsed.lightStates) {
model.lightStates.reserve(parsed.lightStates->size());
for (double v : *parsed.lightStates) {
model.lightStates.push_back(static_cast<float>(v));
}
}
if (parsed.lightColors) {
model.lightColors.reserve(parsed.lightColors->size());
for (const MaterialRgb& rgb : *parsed.lightColors) {
const auto pack = [](double c) {
return static_cast<std::uint32_t>(std::clamp(c, 0.0, 255.0));
};
const std::uint32_t r = pack(rgb.r);
const std::uint32_t g = pack(rgb.g);
const std::uint32_t b = pack(rgb.b);
model.lightColors.push_back((r << 16) | (g << 8) | b);
}
}
model.node.area.center = model.location;
return model;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,164 @@
#pragma once
// Jeden plik tekstowy (.scn/.inc) → jeden moduł runtime (RuntimeModule).
#include <eu07/scene/bake/dynamic.hpp>
#include <eu07/scene/bake/event.hpp>
#include <eu07/scene/bake/eventlauncher.hpp>
#include <eu07/scene/bake/lines.hpp>
#include <eu07/scene/bake/memcell.hpp>
#include <eu07/scene/bake/mesh.hpp>
#include <eu07/scene/bake/model.hpp>
#include <eu07/scene/bake/sound.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/bake/traction.hpp>
#include <eu07/scene/bake/traction_power.hpp>
#include <eu07/scene/bake/trainset.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/include_placement.hpp>
#include <eu07/scene/include_types.hpp>
#include <eu07/scene/runtime/scene.hpp>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
namespace eu07::scene::bake {
struct ModuleInclude {
std::uint32_t sourceLine = 0;
std::string sourcePath;
std::string binaryPath;
std::vector<std::string> parameters;
runtime::TransformContext siteTransform;
};
struct RuntimeModule {
std::vector<ModuleInclude> includes;
runtime::RuntimeScene scene;
IncludePlacement includePlacement;
};
struct BakeModuleOptions {
// Root + PACK: modele i tak pochodza z composePackModels.
bool skipLocalModels = false;
};
[[nodiscard]] inline std::string textPathToBinaryPath(const std::string& textPath) {
std::filesystem::path path{textPath};
path.replace_extension(".eu7");
return path.generic_string();
}
[[nodiscard]] inline RuntimeModule bakeModule(
const SceneDocument& document,
const BakeModuleOptions& options = {}) {
RuntimeModule module;
module.includes.reserve(document.include.size());
for (const ParsedInclude& inc : document.include) {
ModuleInclude ref;
ref.sourceLine = static_cast<std::uint32_t>(inc.line);
ref.sourcePath = inc.file;
ref.binaryPath = textPathToBinaryPath(inc.file);
ref.parameters = inc.parameters;
ref.siteTransform = inc.siteTransform;
module.includes.push_back(std::move(ref));
}
auto& scene = module.scene;
scene.tracks.reserve(countTrackInstances(document));
for (const ParsedTrackNormal& parsed : document.nodeTrackNormal) {
scene.tracks.push_back(bakeTrackNormal(parsed));
}
for (const ParsedTrackSwitch& parsed : document.nodeTrackSwitch) {
scene.tracks.push_back(bakeTrackSwitch(parsed));
}
for (const ParsedTrackRoad& parsed : document.nodeTrackRoad) {
scene.tracks.push_back(bakeTrackRoad(parsed));
}
for (const ParsedTrackCross& parsed : document.nodeTrackCross) {
scene.tracks.push_back(bakeTrackCross(parsed));
}
for (const ParsedTrackOther& parsed : document.nodeTrackOther) {
scene.tracks.push_back(bakeTrackOther(parsed));
}
scene.traction.reserve(document.nodeTraction.size());
for (const ParsedNodeTraction& parsed : document.nodeTraction) {
scene.traction.push_back(bakeTraction(parsed));
}
scene.powerSources.reserve(document.nodeTractionPower.size());
for (const ParsedNodeTractionPowerSource& parsed : document.nodeTractionPower) {
scene.powerSources.push_back(bakeTractionPower(parsed));
}
const std::size_t shapeCount =
document.nodeTriangles.size() + document.nodeTriangleStrip.size() +
document.nodeTriangleFan.size();
scene.shapes.reserve(shapeCount);
for (const ParsedNodeTriangles& parsed : document.nodeTriangles) {
scene.shapes.push_back(bakeTriangles(parsed));
}
for (const ParsedNodeTriangleStrip& parsed : document.nodeTriangleStrip) {
scene.shapes.push_back(bakeTriangleStrip(parsed));
}
for (const ParsedNodeTriangleFan& parsed : document.nodeTriangleFan) {
scene.shapes.push_back(bakeTriangleFan(parsed));
}
const std::size_t lineCount =
document.nodeLines.size() + document.nodeLineStrip.size() + document.nodeLineLoop.size();
scene.lines.reserve(lineCount);
for (const ParsedNodeLines& parsed : document.nodeLines) {
scene.lines.push_back(bakeLines(parsed));
}
for (const ParsedNodeLineStrip& parsed : document.nodeLineStrip) {
scene.lines.push_back(bakeLineStrip(parsed));
}
for (const ParsedNodeLineLoop& parsed : document.nodeLineLoop) {
scene.lines.push_back(bakeLineLoop(parsed));
}
if (!options.skipLocalModels) {
scene.models.reserve(document.nodeModel.size());
for (const ParsedNodeModel& parsed : document.nodeModel) {
scene.models.push_back(bakeModel(parsed));
}
}
scene.memcells.reserve(document.nodeMemcell.size());
for (const ParsedNodeMemcell& parsed : document.nodeMemcell) {
scene.memcells.push_back(bakeMemcell(parsed));
}
scene.eventLaunchers.reserve(document.nodeEventlauncher.size());
for (const ParsedNodeEventlauncher& parsed : document.nodeEventlauncher) {
scene.eventLaunchers.push_back(bakeEventlauncher(parsed));
}
scene.dynamics.reserve(document.nodeDynamic.size());
for (const ParsedNodeDynamic& parsed : document.nodeDynamic) {
scene.dynamics.push_back(bakeDynamic(parsed));
}
scene.sounds.reserve(document.nodeSound.size());
for (const ParsedNodeSound& parsed : document.nodeSound) {
scene.sounds.push_back(bakeSound(parsed));
}
scene.trainsets = bakeTrainsets(document, scene.dynamics);
scene.events = bakeEvents(document);
scene.firstInitCount = static_cast<std::uint32_t>(document.firstInit.size());
if (const std::optional<IncludePlacement> placement = extractIncludePlacement(document)) {
module.includePlacement = *placement;
}
return module;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,22 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/sound.hpp>
#include <eu07/scene/runtime/nodes.hpp>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::RuntimeSoundSource bakeSound(const ParsedNodeSound& parsed) {
runtime::RuntimeSoundSource sound;
sound.node = bakeBasicNode(parsed.header, "sound");
sound.wavFile = parsed.wavFile;
const runtime::RuntimeScratchpad scratch = scratchFromHeader(parsed.header);
sound.location = runtime::transformPoint(parsed.position, scratch);
sound.node.area.center = sound.location;
return sound;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,330 @@
#pragma once
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/track.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <format>
#include <limits>
#include <optional>
#include <string>
namespace eu07::scene::bake {
namespace detail {
inline void appendTrackEventKeyword(
std::vector<std::pair<std::string, std::string>>& tail,
const std::string_view key,
const std::optional<std::string>& value) {
if (value && !value->empty()) {
tail.emplace_back(std::string(key), *value);
}
}
inline void appendTrackOptionals(
std::vector<std::pair<std::string, std::string>>& tail,
const TrackOptionals& optionals) {
appendTrackEventKeyword(tail, "event0", optionals.events.occupiedStop);
appendTrackEventKeyword(tail, "eventall0", optionals.events.anyStop);
appendTrackEventKeyword(tail, "event1", optionals.events.occupiedEnterP1);
appendTrackEventKeyword(tail, "eventall1", optionals.events.anyEnterP1);
appendTrackEventKeyword(tail, "event2", optionals.events.occupiedEnterP2);
appendTrackEventKeyword(tail, "eventall2", optionals.events.anyEnterP2);
if (optionals.velocity) {
tail.emplace_back("velocity", std::format("{}", *optionals.velocity));
}
for (const std::string& isolated : optionals.isolated) {
tail.emplace_back("isolated", isolated);
}
if (optionals.overhead) {
tail.emplace_back("overhead", std::format("{}", *optionals.overhead));
}
if (optionals.verticalRadius) {
tail.emplace_back("vradius", std::format("{}", *optionals.verticalRadius));
}
if (optionals.railProfile) {
tail.emplace_back("railprofile", *optionals.railProfile);
}
if (optionals.trackBed) {
tail.emplace_back("trackbed", *optionals.trackBed);
}
if (optionals.frictionOverride) {
tail.emplace_back("friction", std::format("{}", *optionals.frictionOverride));
}
if (optionals.fouling1) {
tail.emplace_back("fouling1", *optionals.fouling1);
}
if (optionals.fouling2) {
tail.emplace_back("fouling2", *optionals.fouling2);
}
if (optionals.sleeperModel) {
const TrackSleeperModel& sleeper = *optionals.sleeperModel;
tail.emplace_back(
"sleepermodel",
std::format(
"{} {} {} {} {} {} {}",
sleeper.spacing,
sleeper.model,
sleeper.texture,
sleeper.offsetLateral,
sleeper.offsetAlong,
sleeper.offsetVertical,
sleeper.bedLowering));
}
for (const auto& [key, value] : optionals.extraKeywords) {
tail.emplace_back(key, value);
}
}
inline void bakeTrackTailKeywords(std::vector<std::pair<std::string, std::string>>& tail, const TrackOptionals& optionals) {
tail.clear();
appendTrackOptionals(tail, optionals);
}
} // namespace detail
[[nodiscard]] inline runtime::BasicNode bakeBasicNode(const NodeHeader& header, const std::string& nodeType) {
runtime::BasicNode node;
node.name = (header.name == "none" || header.name.empty()) ? std::string{} : header.name;
node.nodeType = nodeType;
node.rangeSquaredMin = header.rangeMin * header.rangeMin;
node.rangeSquaredMax =
header.rangeMax >= 0.0 ? header.rangeMax * header.rangeMax
: std::numeric_limits<double>::max();
node.visible = true;
if (header.groupHandle) {
node.groupHandle = *header.groupHandle;
node.groupValid = true;
}
node.transform.originStack = header.originStack;
node.transform.scaleStack = header.scaleStack;
node.transform.rotation = header.rotation;
node.transform.groupStackDepth = header.groupStackDepth;
return node;
}
[[nodiscard]] inline runtime::TrackType mapTrackType(const std::string& kind) {
if (kind == "normal") {
return runtime::TrackType::Normal;
}
if (kind == "switch") {
return runtime::TrackType::Switch;
}
if (kind == "turn" || kind == "table") {
return runtime::TrackType::Table;
}
if (kind == "cross") {
return runtime::TrackType::Cross;
}
if (kind == "tributary") {
return runtime::TrackType::Tributary;
}
return runtime::TrackType::Unknown;
}
[[nodiscard]] inline runtime::TrackCategory mapTrackCategory(const std::string& kind) {
if (kind == "road" || kind == "cross") {
return runtime::TrackCategory::Road;
}
if (kind == "river" || kind == "tributary") {
return runtime::TrackCategory::Water;
}
return runtime::TrackCategory::Rail;
}
[[nodiscard]] inline runtime::TrackEnvironment mapEnvironment(const std::string& env) {
if (env == "flat") {
return runtime::TrackEnvironment::Flat;
}
if (env == "mountains" || env == "mountain") {
return runtime::TrackEnvironment::Mountains;
}
if (env == "canyon") {
return runtime::TrackEnvironment::Canyon;
}
if (env == "tunnel") {
return runtime::TrackEnvironment::Tunnel;
}
if (env == "bridge") {
return runtime::TrackEnvironment::Bridge;
}
if (env == "bank") {
return runtime::TrackEnvironment::Bank;
}
return runtime::TrackEnvironment::Unknown;
}
[[nodiscard]] inline runtime::SegmentPath bakeSegment(
const TrackBezier& bez,
const Vec3& originOffset) {
runtime::SegmentPath seg;
seg.pStart = {bez.p1.x + originOffset.x, bez.p1.y + originOffset.y, bez.p1.z + originOffset.z};
seg.rollStart = bez.roll1;
seg.cpOut = bez.cv1;
seg.cpIn = bez.cv2;
seg.pEnd = {bez.p2.x + originOffset.x, bez.p2.y + originOffset.y, bez.p2.z + originOffset.z};
seg.rollEnd = bez.roll2;
seg.radius = bez.radius;
return seg;
}
[[nodiscard]] inline void appendSegments(
runtime::RuntimeTrack& track,
const std::vector<TrackBezier>& beziers,
const Vec3& originOffset) {
track.paths.reserve(track.paths.size() + beziers.size());
for (const TrackBezier& bez : beziers) {
track.paths.push_back(bakeSegment(bez, originOffset));
}
}
[[nodiscard]] inline runtime::RuntimeTrack bakeTrackNormal(const ParsedTrackNormal& parsed) {
runtime::RuntimeTrack track;
track.node = bakeBasicNode(parsed.header, "track");
track.trackType = mapTrackType(parsed.trajectoryKind);
track.category = mapTrackCategory(parsed.trajectoryKind);
track.length = static_cast<float>(parsed.length);
track.trackWidth = static_cast<float>(parsed.gauge);
track.friction = static_cast<float>(parsed.friction);
track.soundDistance = static_cast<float>(parsed.soundDist);
track.qualityFlag = parsed.quality;
track.damageFlag = parsed.damageFlag;
track.environment = mapEnvironment(parsed.environment);
if (parsed.visibilityMode == TrackVisibilityMode::Visible && parsed.visibility) {
runtime::TrackVisibility vis;
vis.material1 = parsed.visibility->railTexture;
vis.texLength = static_cast<float>(parsed.visibility->railTexLength);
vis.material2 = parsed.visibility->ballastTexture;
vis.texHeight1 = static_cast<float>(parsed.visibility->ballastHeight);
vis.texWidth = static_cast<float>(parsed.visibility->ballastWidth);
vis.texSlope = static_cast<float>(parsed.visibility->ballastSlope);
track.visibility = vis;
}
appendSegments(track, parsed.beziers, parsed.header.originOffset);
detail::bakeTrackTailKeywords(track.tailKeywords, parsed.optionals);
return track;
}
[[nodiscard]] inline runtime::RuntimeTrack bakeTrackSwitch(const ParsedTrackSwitch& parsed) {
runtime::RuntimeTrack track;
track.node = bakeBasicNode(parsed.header, "track");
track.trackType = runtime::TrackType::Switch;
track.category = runtime::TrackCategory::Rail;
track.length = static_cast<float>(parsed.length);
track.trackWidth = static_cast<float>(parsed.gauge);
track.friction = static_cast<float>(parsed.friction);
track.soundDistance = static_cast<float>(parsed.soundDist);
track.qualityFlag = parsed.quality;
track.damageFlag = parsed.damageFlag;
track.environment = mapEnvironment(parsed.environment);
if (parsed.visibilityMode == TrackVisibilityMode::Visible && parsed.visibility) {
runtime::TrackVisibility vis;
vis.material1 = parsed.visibility->mainRailTexture;
vis.texLength = static_cast<float>(parsed.visibility->mainRailTexLength);
vis.material2 = parsed.visibility->switchRailTexture;
vis.texHeight1 = static_cast<float>(parsed.visibility->unusedHeight);
vis.texWidth = static_cast<float>(parsed.visibility->unusedWidth);
vis.texSlope = static_cast<float>(parsed.visibility->unusedSlope);
track.visibility = vis;
}
appendSegments(track, parsed.beziers, parsed.header.originOffset);
detail::bakeTrackTailKeywords(track.tailKeywords, parsed.optionals);
return track;
}
[[nodiscard]] inline runtime::RuntimeTrack bakeTrackRoad(const ParsedTrackRoad& parsed) {
runtime::RuntimeTrack track;
track.node = bakeBasicNode(parsed.header, "track");
track.trackType = runtime::TrackType::Normal;
track.category = runtime::TrackCategory::Road;
track.length = static_cast<float>(parsed.length);
track.trackWidth = static_cast<float>(parsed.roadWidth);
track.friction = static_cast<float>(parsed.friction);
track.soundDistance = static_cast<float>(parsed.soundDist);
track.qualityFlag = parsed.quality;
track.damageFlag = parsed.damageFlag;
track.environment = mapEnvironment(parsed.environment);
if (parsed.visibilityMode == TrackVisibilityMode::Visible && parsed.visibility) {
runtime::TrackVisibility vis;
vis.material1 = parsed.visibility->surfaceTexture;
vis.texLength = static_cast<float>(parsed.visibility->surfaceTexLength);
vis.material2 = parsed.visibility->shoulderTexture;
vis.texHeight1 = static_cast<float>(parsed.visibility->shoulderHeight);
vis.texWidth = static_cast<float>(parsed.visibility->shoulderWidth);
vis.texSlope = static_cast<float>(parsed.visibility->shoulderSlope);
track.visibility = vis;
}
appendSegments(track, parsed.beziers, parsed.header.originOffset);
detail::bakeTrackTailKeywords(track.tailKeywords, parsed.optionals);
return track;
}
[[nodiscard]] inline runtime::RuntimeTrack bakeTrackCross(const ParsedTrackCross& parsed) {
runtime::RuntimeTrack track;
track.node = bakeBasicNode(parsed.header, "track");
track.trackType = runtime::TrackType::Cross;
track.category = runtime::TrackCategory::Road;
track.length = static_cast<float>(parsed.length);
track.trackWidth = static_cast<float>(parsed.roadWidth);
track.friction = static_cast<float>(parsed.friction);
track.soundDistance = static_cast<float>(parsed.soundDist);
track.qualityFlag = parsed.quality;
track.damageFlag = parsed.damageFlag;
track.environment = mapEnvironment(parsed.environment);
if (parsed.visibilityMode == TrackVisibilityMode::Visible && parsed.visibility) {
runtime::TrackVisibility vis;
vis.material1 = parsed.visibility->surfaceTexture;
vis.texLength = static_cast<float>(parsed.visibility->surfaceTexLength);
vis.material2 = parsed.visibility->shoulderTexture;
vis.texHeight1 = static_cast<float>(parsed.visibility->shoulderHeight);
vis.texWidth = static_cast<float>(parsed.visibility->shoulderWidth);
vis.texSlope = static_cast<float>(parsed.visibility->shoulderSlope);
track.visibility = vis;
}
appendSegments(track, parsed.beziers, parsed.header.originOffset);
detail::bakeTrackTailKeywords(track.tailKeywords, parsed.optionals);
return track;
}
[[nodiscard]] inline runtime::RuntimeTrack bakeTrackOther(const ParsedTrackOther& parsed) {
runtime::RuntimeTrack track;
track.node = bakeBasicNode(parsed.header, "track");
track.trackType = mapTrackType(parsed.trajectoryKind);
track.category = mapTrackCategory(parsed.trajectoryKind);
track.length = static_cast<float>(parsed.length);
track.trackWidth = static_cast<float>(parsed.width);
track.friction = static_cast<float>(parsed.friction);
track.soundDistance = static_cast<float>(parsed.soundDist);
track.qualityFlag = parsed.quality;
track.damageFlag = parsed.damageFlag;
track.environment = mapEnvironment(parsed.environment);
if (parsed.visibilityMode == TrackVisibilityMode::Visible) {
if (parsed.riverVisibility) {
runtime::TrackVisibility vis;
vis.material1 = parsed.riverVisibility->waterTexture;
vis.texLength = static_cast<float>(parsed.riverVisibility->waterTexLength);
vis.material2 = parsed.riverVisibility->bankTexture;
vis.texHeight1 = static_cast<float>(parsed.riverVisibility->bankHeight);
vis.texWidth = static_cast<float>(parsed.riverVisibility->leftBankWidth);
vis.texSlope = static_cast<float>(parsed.riverVisibility->rightBankWidth);
track.visibility = vis;
} else if (parsed.railVisibility) {
runtime::TrackVisibility vis;
vis.material1 = parsed.railVisibility->railTexture;
vis.texLength = static_cast<float>(parsed.railVisibility->railTexLength);
vis.material2 = parsed.railVisibility->ballastTexture;
vis.texHeight1 = static_cast<float>(parsed.railVisibility->ballastHeight);
vis.texWidth = static_cast<float>(parsed.railVisibility->ballastWidth);
vis.texSlope = static_cast<float>(parsed.railVisibility->ballastSlope);
track.visibility = vis;
}
}
appendSegments(track, parsed.beziers, parsed.header.originOffset);
detail::bakeTrackTailKeywords(track.tailKeywords, parsed.optionals);
return track;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,64 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/traction.hpp>
#include <eu07/scene/runtime/nodes.hpp>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::TractionWireMaterial mapWireMaterial(const TractionMaterialKind kind) {
switch (kind) {
case TractionMaterialKind::None:
return runtime::TractionWireMaterial::None;
case TractionMaterialKind::Aluminum:
return runtime::TractionWireMaterial::Aluminium;
default:
return runtime::TractionWireMaterial::Copper;
}
}
[[nodiscard]] inline Vec3 offsetPoint(const Vec3& point, const Vec3& origin) {
return {point.x + origin.x, point.y + origin.y, point.z + origin.z};
}
[[nodiscard]] inline runtime::RuntimeTraction bakeTraction(const ParsedNodeTraction& parsed) {
runtime::RuntimeTraction traction;
traction.node = bakeBasicNode(parsed.header, "traction");
traction.powerSupplyName = parsed.powerSourceName;
traction.nominalVoltage = static_cast<float>(parsed.nominalVoltage);
traction.maxCurrent = static_cast<float>(parsed.maxCurrent);
traction.resistivityLegacy = parsed.resistivity;
double resistivity = parsed.resistivity;
if (resistivity == 0.01) {
resistivity = 0.075;
}
traction.resistivityOhmPerM = static_cast<float>(resistivity * 0.001);
traction.materialRaw = parsed.materialRaw;
traction.material = mapWireMaterial(parsed.materialKind);
traction.wireThickness = static_cast<float>(parsed.wireThickness);
traction.damageFlag = parsed.damageFlag;
const Vec3& origin = parsed.header.originOffset;
traction.wireP1 = offsetPoint(parsed.wireLowerStart, origin);
traction.wireP2 = offsetPoint(parsed.wireLowerEnd, origin);
traction.wireP3 = offsetPoint(parsed.wireUpperStart, origin);
traction.wireP4 = offsetPoint(parsed.wireUpperEnd, origin);
traction.minHeight = parsed.minHeight;
traction.segmentLength = parsed.segmentLength;
traction.wireCount = parsed.wires;
traction.wireOffset = static_cast<float>(parsed.wireOffset);
traction.parallelName = parsed.parallel;
traction.node.visible = parsed.visibilityMode == NodeVisibilityMode::Visible;
traction.node.area.center = {
(traction.wireP2.x + traction.wireP1.x) * 0.5,
(traction.wireP2.y + traction.wireP1.y) * 0.5,
(traction.wireP2.z + traction.wireP1.z) * 0.5};
return traction;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,48 @@
#pragma once
#include <eu07/scene/bake/geometry.hpp>
#include <eu07/scene/bake/track.hpp>
#include <eu07/scene/node/traction_power.hpp>
#include <eu07/scene/runtime/nodes.hpp>
namespace eu07::scene::bake {
[[nodiscard]] inline runtime::PowerSourceModifier mapPowerModifier(
const ParsedNodeTractionPowerSource& parsed) {
if (parsed.section) {
return runtime::PowerSourceModifier::Section;
}
if (parsed.recuperation) {
return runtime::PowerSourceModifier::Recuperation;
}
return runtime::PowerSourceModifier::None;
}
[[nodiscard]] inline runtime::RuntimeTractionPowerSource bakeTractionPower(
const ParsedNodeTractionPowerSource& parsed) {
runtime::RuntimeTractionPowerSource source;
source.node = bakeBasicNode(parsed.header, "tractionpowersource");
const runtime::RuntimeScratchpad scratch = scratchFromHeader(parsed.header);
source.position = runtime::transformPoint(parsed.position, scratch);
source.node.area.center = source.position;
source.nominalVoltage = static_cast<float>(parsed.nominalVoltage);
source.voltageFrequency = static_cast<float>(parsed.voltageFrequency);
source.maxOutputCurrent = static_cast<float>(parsed.maxOutputCurrent);
source.fastFuseTimeout = static_cast<float>(parsed.fastFuseTimeout);
source.fastFuseRepetition = static_cast<float>(parsed.fastFuseRepetition);
source.slowFuseTimeout = static_cast<float>(parsed.slowFuseTimeout);
source.modifier = mapPowerModifier(parsed);
source.internalResistanceLegacy = parsed.internalResistance;
float internalResistance = static_cast<float>(parsed.internalResistance);
if (internalResistance < 0.1f) {
internalResistance = 0.2f;
}
source.internalResistance = internalResistance;
return source;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,77 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/runtime/directives.hpp>
#include <cstddef>
#include <string>
#include <vector>
namespace eu07::scene::bake {
namespace detail {
[[nodiscard]] inline bool parseTrainsetHeader(
const DirectiveBlock& block,
runtime::RuntimeTrainset& trainset) {
TokenStream stream(block.tokens);
if (stream.empty() || !isKeyword(stream.peek().value, "trainset")) {
return false;
}
stream.consume();
std::vector<SourceToken> raw;
std::string name;
std::string track;
double offset = 0.0;
double velocity = 0.0;
if (!node::io::takeString(stream, raw, name) || !node::io::takeString(stream, raw, track) ||
!node::io::takeDouble(stream, raw, offset) ||
!node::io::takeDouble(stream, raw, velocity)) {
return false;
}
trainset.name = std::move(name);
trainset.track = std::move(track);
trainset.offset = static_cast<float>(offset);
trainset.velocity = static_cast<float>(velocity);
return true;
}
} // namespace detail
[[nodiscard]] inline std::vector<runtime::RuntimeTrainset> bakeTrainsets(
const SceneDocument& document,
const std::vector<runtime::RuntimeDynamicObject>& dynamics) {
std::vector<runtime::RuntimeTrainset> trainsets;
trainsets.reserve(document.trainset.size());
for (std::size_t trainsetIdx = 0; trainsetIdx < document.trainset.size(); ++trainsetIdx) {
runtime::RuntimeTrainset trainset;
if (!detail::parseTrainsetHeader(document.trainset[trainsetIdx], trainset)) {
continue;
}
for (std::size_t dynIdx = 0; dynIdx < dynamics.size(); ++dynIdx) {
const runtime::RuntimeDynamicObject& vehicle = dynamics[dynIdx];
if (vehicle.trainsetIndex != trainsetIdx) {
continue;
}
trainset.vehicleIndices.push_back(dynIdx);
trainset.couplings.push_back(vehicle.coupling);
if (trainset.driverIndex == static_cast<std::size_t>(-1) &&
!vehicle.driverType.empty() && vehicle.driverType != "0") {
trainset.driverIndex = trainset.vehicleIndices.size() - 1;
}
}
trainsets.push_back(std::move(trainset));
}
return trainsets;
}
} // namespace eu07::scene::bake

View File

@@ -0,0 +1,272 @@
#pragma once
// EU7B v1 — binarna scena (SCM/SCN + referencje include, bez rozwijania INC).
// Little-endian. Bez NMT, bez mesh/e3d.
#include <eu07/scene/document.hpp>
#include <eu07/scene/include_types.hpp>
#include <eu07/scene/node/track.hpp>
#include <eu07/scene/binary/common.hpp>
#include <eu07/scene/binary/io.hpp>
#include <array>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace eu07::scene::binary {
inline constexpr std::uint32_t kVersion = kVersionLegacy;
inline constexpr std::array<char, 4> kChunkIncl{'I', 'N', 'C', 'L'};
inline constexpr std::array<char, 4> kChunkTrak{'T', 'R', 'A', 'K'};
enum class TrackKind : std::uint8_t {
Normal = 0,
Switch = 1,
Road = 2,
Cross = 3,
Other = 4,
};
struct LoadedSceneBinary {
SceneDocument document;
std::uint32_t version = 0;
struct ChunkSummary {
std::array<char, 4> id{};
std::uint32_t size = 0;
bool recognized = false;
};
std::vector<ChunkSummary> chunks;
};
namespace detail {
inline void writeBezier(std::ostream& out, const TrackBezier& bez) {
io::writeVec3(out, bez.p1);
io::writeVec3(out, bez.cv1);
io::writeVec3(out, bez.cv2);
io::writeVec3(out, bez.p2);
io::writeF64(out, bez.roll1);
io::writeF64(out, bez.roll2);
io::writeF64(out, bez.radius);
}
inline void writeIncludeRecord(std::ostream& out, const ParsedInclude& inc) {
io::writeU32(out, static_cast<std::uint32_t>(inc.line));
io::writeU32(out, static_cast<std::uint32_t>(inc.file.size()));
out.write(inc.file.data(), static_cast<std::streamsize>(inc.file.size()));
io::writeU32(out, static_cast<std::uint32_t>(inc.parameters.size()));
for (const std::string& param : inc.parameters) {
io::writeU32(out, static_cast<std::uint32_t>(param.size()));
out.write(param.data(), static_cast<std::streamsize>(param.size()));
}
}
[[nodiscard]] inline TrackBezier readBezier(std::istream& in) {
TrackBezier bez;
bez.p1 = io::readVec3(in);
bez.cv1 = io::readVec3(in);
bez.cv2 = io::readVec3(in);
bez.p2 = io::readVec3(in);
bez.roll1 = io::readF64(in);
bez.roll2 = io::readF64(in);
bez.radius = io::readF64(in);
return bez;
}
[[nodiscard]] inline ParsedInclude readIncludeRecord(std::istream& in) {
ParsedInclude inc;
inc.line = io::readU32(in);
inc.file = io::readString(in);
const std::uint32_t paramCount = io::readU32(in);
inc.parameters.reserve(paramCount);
for (std::uint32_t i = 0; i < paramCount; ++i) {
inc.parameters.push_back(io::readString(in));
}
inc.expanded = false;
return inc;
}
template <typename TrackNode>
inline void writeTrackRecord(std::ostream& out, const TrackKind kind, const TrackNode& node) {
io::writeU8(out, static_cast<std::uint8_t>(kind));
io::writeU32(out, static_cast<std::uint32_t>(node.header.name.size()));
out.write(node.header.name.data(), static_cast<std::streamsize>(node.header.name.size()));
io::writeVec3(out, node.header.originOffset);
io::writeF64(out, node.length);
io::writeU32(out, static_cast<std::uint32_t>(node.beziers.size()));
for (const TrackBezier& bez : node.beziers) {
writeBezier(out, bez);
}
}
template <typename TrackNode, typename PushFn>
inline void readTrackBody(std::istream& in, PushFn push) {
TrackNode node;
node.header.name = io::readString(in);
node.header.originOffset = io::readVec3(in);
node.length = io::readF64(in);
const std::uint32_t bezCount = io::readU32(in);
node.beziers.reserve(bezCount);
for (std::uint32_t i = 0; i < bezCount; ++i) {
node.beziers.push_back(readBezier(in));
}
push(std::move(node));
}
[[nodiscard]] inline std::vector<char> buildInclPayload(const SceneDocument& doc) {
std::ostringstream inclOut(std::ios::binary);
io::writeU32(inclOut, static_cast<std::uint32_t>(doc.include.size()));
for (const ParsedInclude& inc : doc.include) {
writeIncludeRecord(inclOut, inc);
}
const std::string blob = inclOut.str();
return {blob.begin(), blob.end()};
}
[[nodiscard]] inline std::vector<char> buildTrakPayload(const SceneDocument& doc) {
std::ostringstream trakOut(std::ios::binary);
for (const ParsedTrackNormal& node : doc.nodeTrackNormal) {
writeTrackRecord(trakOut, TrackKind::Normal, node);
}
for (const ParsedTrackSwitch& node : doc.nodeTrackSwitch) {
writeTrackRecord(trakOut, TrackKind::Switch, node);
}
for (const ParsedTrackRoad& node : doc.nodeTrackRoad) {
writeTrackRecord(trakOut, TrackKind::Road, node);
}
for (const ParsedTrackCross& node : doc.nodeTrackCross) {
writeTrackRecord(trakOut, TrackKind::Cross, node);
}
for (const ParsedTrackOther& node : doc.nodeTrackOther) {
writeTrackRecord(trakOut, TrackKind::Other, node);
}
const std::string blob = trakOut.str();
return {blob.begin(), blob.end()};
}
} // namespace detail
inline void writeSceneBinary(const std::filesystem::path& outPath, const SceneDocument& doc) {
const std::vector<char> inclPayload = detail::buildInclPayload(doc);
const std::vector<char> trakPayload = detail::buildTrakPayload(doc);
std::ofstream out(outPath, std::ios::binary | std::ios::trunc);
if (!out) {
throw std::runtime_error("Nie mozna zapisac: " + outPath.string());
}
out.write(kMagic.data(), 4);
io::writeU32(out, kVersion);
io::writeChunkHeader(out, kChunkIncl, 8 + static_cast<std::uint32_t>(inclPayload.size()));
out.write(inclPayload.data(), static_cast<std::streamsize>(inclPayload.size()));
io::writeChunkHeader(out, kChunkTrak, 8 + static_cast<std::uint32_t>(trakPayload.size()));
out.write(trakPayload.data(), static_cast<std::streamsize>(trakPayload.size()));
}
[[nodiscard]] inline LoadedSceneBinary readSceneBinary(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("Nie mozna otworzyc: " + path.string());
}
std::array<char, 4> magic{};
in.read(magic.data(), 4);
if (!in || magic != kMagic) {
throw std::runtime_error("EU7B: zly magic");
}
const std::uint32_t version = io::readU32(in);
if (version != kVersion) {
throw std::runtime_error("EU7B: nieobslugiwana wersja " + std::to_string(version));
}
LoadedSceneBinary loaded;
loaded.document = SceneDocument{};
loaded.version = version;
while (in.peek() != EOF) {
std::array<char, 4> chunkId{};
in.read(chunkId.data(), 4);
if (!in) {
break;
}
const std::uint32_t chunkSize = io::readU32(in);
if (chunkSize < 8) {
throw std::runtime_error("EU7B: uszkodzony chunk");
}
const std::uint32_t payloadSize = chunkSize - 8;
LoadedSceneBinary::ChunkSummary chunkSummary;
chunkSummary.id = chunkId;
chunkSummary.size = chunkSize;
if (chunkId == kChunkIncl) {
chunkSummary.recognized = true;
const auto chunkStart = in.tellg();
const std::uint32_t count = io::readU32(in);
loaded.document.include.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
loaded.document.include.push_back(detail::readIncludeRecord(in));
}
if (in.tellg() - chunkStart != static_cast<std::streamoff>(payloadSize)) {
throw std::runtime_error("EU7B: uszkodzony chunk INCL");
}
loaded.chunks.push_back(chunkSummary);
continue;
}
if (chunkId == kChunkTrak) {
chunkSummary.recognized = true;
const auto chunkStart = in.tellg();
while (in.tellg() - chunkStart < static_cast<std::streamoff>(payloadSize)) {
switch (static_cast<TrackKind>(io::readU8(in))) {
case TrackKind::Normal:
detail::readTrackBody<ParsedTrackNormal>(in, [&](ParsedTrackNormal node) {
loaded.document.nodeTrackNormal.push_back(std::move(node));
});
break;
case TrackKind::Switch:
detail::readTrackBody<ParsedTrackSwitch>(in, [&](ParsedTrackSwitch node) {
loaded.document.nodeTrackSwitch.push_back(std::move(node));
});
break;
case TrackKind::Road:
detail::readTrackBody<ParsedTrackRoad>(in, [&](ParsedTrackRoad node) {
loaded.document.nodeTrackRoad.push_back(std::move(node));
});
break;
case TrackKind::Cross:
detail::readTrackBody<ParsedTrackCross>(in, [&](ParsedTrackCross node) {
loaded.document.nodeTrackCross.push_back(std::move(node));
});
break;
case TrackKind::Other:
detail::readTrackBody<ParsedTrackOther>(in, [&](ParsedTrackOther node) {
loaded.document.nodeTrackOther.push_back(std::move(node));
});
break;
default:
throw std::runtime_error("EU7B: nieznany typ toru");
}
}
loaded.chunks.push_back(chunkSummary);
continue;
}
in.seekg(static_cast<std::streamoff>(payloadSize), std::ios::cur);
loaded.chunks.push_back(chunkSummary);
}
return loaded;
}
} // namespace eu07::scene::binary

View File

@@ -0,0 +1,119 @@
#pragma once
#include <eu07/scene/binary/io.hpp>
#include <array>
#include <cctype>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
namespace eu07::scene::binary {
inline constexpr std::array<char, 4> kMagic{'E', 'U', '7', 'B'};
// Pole version w EU7B: 5 = TERR terrain-lite. 4 = slim node + packed mesh. 3 = float32. 2 = float64.
inline constexpr std::uint32_t kVersionLegacy = 1; // codec.hpp — nieobslugiwane w CLI
inline constexpr std::uint32_t kVersionRuntime = 6;
inline constexpr std::uint32_t kVersionRuntimeV7 = 7;
inline constexpr std::uint32_t kVersionRuntimeV5 = 5;
inline constexpr std::uint32_t kVersionRuntimeV4 = 4;
inline constexpr std::uint32_t kVersionRuntimeF32 = 3;
inline constexpr std::uint32_t kVersionRuntimeF64 = 2;
[[nodiscard]] inline bool isSupportedEu7bVersion(const std::uint32_t version) noexcept {
return version == kVersionRuntimeV7 || version == kVersionRuntime ||
version == kVersionRuntimeV5 || version == kVersionRuntimeV4;
}
[[nodiscard]] inline std::string formatVersionName(const std::uint32_t version) {
if (version == kVersionRuntimeV7) {
return "RuntimeV7";
}
if (version == kVersionRuntime) {
return "RuntimeV6";
}
if (version == kVersionRuntimeV5) {
return "RuntimeV5";
}
if (version == kVersionRuntimeV4) {
return "RuntimeV4";
}
if (version == kVersionRuntimeF32) {
return "RuntimeF32";
}
if (version == kVersionRuntimeF64) {
return "RuntimeF64";
}
if (version == kVersionLegacy) {
return "Legacy";
}
return "Unknown";
}
[[nodiscard]] inline std::string formatVersionDescription(const std::uint32_t version) {
if (version == kVersionRuntimeV7) {
return "bake v6 + MODL w chunkach PIDX/PACK per sekcja 1km (v7)";
}
if (version == kVersionRuntime) {
return "bake + chunki STRS/INCL/TRAK/... + INCL site transform (v6)";
}
if (version == kVersionRuntimeV5) {
return "bake + chunki STRS/INCL/TRAK/... + TERR batched per sekcja 1km (v5)";
}
if (version == kVersionRuntimeV4) {
return "bake slim v4 — przebakeuj do v5 dla TERR";
}
if (version == kVersionRuntimeF32) {
return "bake float32 v3 — przebakeuj do v4";
}
if (version == kVersionRuntimeF64) {
return "bake float64 v2 — przebakeuj do v4";
}
if (version == kVersionLegacy) {
return "legacy (nieobslugiwane w CLI)";
}
return "nieobslugiwana wersja " + std::to_string(version);
}
[[nodiscard]] inline bool isSceneBinaryPath(const std::filesystem::path& path) {
const std::string ext = path.extension().string();
if (ext.empty()) {
return false;
}
std::string lower = ext;
for (char& ch : lower) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
}
return lower == ".eu7";
}
[[nodiscard]] inline bool probeSceneBinaryMagic(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
return false;
}
std::array<char, 4> magic{};
in.read(magic.data(), 4);
return in.good() && magic == kMagic;
}
[[nodiscard]] inline bool isSceneBinaryInput(const std::filesystem::path& path) {
return isSceneBinaryPath(path) || probeSceneBinaryMagic(path);
}
[[nodiscard]] inline std::uint32_t peekSceneBinaryVersion(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("Nie mozna otworzyc: " + path.string());
}
std::array<char, 4> magic{};
in.read(magic.data(), 4);
if (!in || magic != kMagic) {
throw std::runtime_error("EU7B: zly magic");
}
return io::readU32(in);
}
} // namespace eu07::scene::binary

View File

@@ -0,0 +1,717 @@
#pragma once
// Runtime → strumień SourceToken (jak po eu07::parseFile) z odtworzonym stosem origin/scale/rotate.
#include <eu07/parser.hpp>
#include <eu07/scene/bake/module.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <eu07/scene/runtime/transform.hpp>
#include <cmath>
#include <filesystem>
#include <format>
#include <limits>
#include <string>
#include <string_view>
#include <vector>
namespace eu07::scene::binary::detokenize {
[[nodiscard]] inline std::string formatDouble(const double value) {
std::string text = std::format("{:.4f}", value);
if (const std::size_t dot = text.find('.'); dot != std::string::npos) {
while (text.size() > dot + 1 && text.back() == '0') {
text.pop_back();
}
if (text.back() == '.') {
text.pop_back();
}
}
return text;
}
class TokenWriter {
public:
std::vector<SourceToken> tokens;
void newline() { ++line_; }
void push(std::string value) { tokens.push_back(SourceToken{std::move(value), line_}); }
void push(std::string_view value) { push(std::string(value)); }
void push(const char* value) { push(std::string_view(value)); }
void push(const double value) { push(formatDouble(value)); }
void push(const float value) { push(static_cast<double>(value)); }
void push(const int value) { push(std::format("{}", value)); }
void pushVec3(const runtime::Vec3& v) {
push(v.x);
push(v.y);
push(v.z);
}
private:
std::size_t line_ = 0;
};
[[nodiscard]] inline std::string includeTextPath(const bake::ModuleInclude& include) {
if (!include.sourcePath.empty()) {
return include.sourcePath;
}
std::filesystem::path path{include.binaryPath};
if (path.extension() == ".eu7") {
path.replace_extension(".inc");
}
return path.generic_string();
}
[[nodiscard]] inline double rangeMaxFromNode(const runtime::BasicNode& node) {
if (node.rangeSquaredMax >= std::numeric_limits<double>::max() * 0.5) {
return -1.0;
}
return std::sqrt(node.rangeSquaredMax);
}
[[nodiscard]] inline double rangeMinFromNode(const runtime::BasicNode& node) {
return std::sqrt(node.rangeSquaredMin);
}
[[nodiscard]] inline std::string nodeDisplayName(const runtime::BasicNode& node) {
return node.name.empty() ? "none" : node.name;
}
inline void pushNodeHeader(TokenWriter& w, const runtime::BasicNode& node) {
w.push("node");
w.push(rangeMaxFromNode(node));
w.push(rangeMinFromNode(node));
w.push(nodeDisplayName(node));
}
struct TransformEmitState {
std::vector<runtime::Vec3> originStack;
std::vector<runtime::Vec3> scaleStack;
runtime::Vec3 rotation{};
std::size_t groupDepth = 0;
};
[[nodiscard]] inline runtime::Vec3 originPushDelta(
const std::vector<runtime::Vec3>& stack,
const std::size_t index) {
const runtime::Vec3& cumulative = stack[index];
if (index == 0) {
return cumulative;
}
const runtime::Vec3& parent = stack[index - 1];
return {cumulative.x - parent.x, cumulative.y - parent.y, cumulative.z - parent.z};
}
[[nodiscard]] inline runtime::Vec3 scalePushFactor(
const std::vector<runtime::Vec3>& stack,
const std::size_t index) {
const runtime::Vec3& cumulative = stack[index];
const runtime::Vec3 parent = index == 0 ? runtime::Vec3{1.0, 1.0, 1.0} : stack[index - 1];
return {
parent.x != 0.0 ? cumulative.x / parent.x : cumulative.x,
parent.y != 0.0 ? cumulative.y / parent.y : cumulative.y,
parent.z != 0.0 ? cumulative.z / parent.z : cumulative.z};
}
inline void syncTransformStack(TokenWriter& w, TransformEmitState& state, const runtime::TransformContext& target) {
while (state.groupDepth > target.groupStackDepth) {
w.push("endgroup");
--state.groupDepth;
}
while (state.scaleStack.size() > target.scaleStack.size()) {
w.push("endscale");
state.scaleStack.pop_back();
}
while (state.originStack.size() > target.originStack.size()) {
w.push("endorigin");
state.originStack.pop_back();
}
while (state.originStack.size() < target.originStack.size()) {
const std::size_t index = state.originStack.size();
const runtime::Vec3 delta = originPushDelta(target.originStack, index);
w.push("origin");
w.pushVec3(delta);
state.originStack.push_back(target.originStack[index]);
}
while (state.scaleStack.size() < target.scaleStack.size()) {
const std::size_t index = state.scaleStack.size();
const runtime::Vec3 factor = scalePushFactor(target.scaleStack, index);
w.push("scale");
w.pushVec3(factor);
state.scaleStack.push_back(target.scaleStack[index]);
}
while (state.groupDepth < target.groupStackDepth) {
w.push("group");
++state.groupDepth;
}
if (state.rotation.x != target.rotation.x || state.rotation.y != target.rotation.y ||
state.rotation.z != target.rotation.z) {
w.push("rotate");
w.pushVec3(target.rotation);
state.rotation = target.rotation;
}
}
inline void emitNodeTransform(TokenWriter& w, TransformEmitState& state, const runtime::BasicNode& node) {
syncTransformStack(w, state, node.transform);
}
[[nodiscard]] inline std::string_view trackEnvironmentName(const runtime::TrackEnvironment env) {
switch (env) {
case runtime::TrackEnvironment::Flat:
return "flat";
case runtime::TrackEnvironment::Mountains:
return "mountains";
case runtime::TrackEnvironment::Canyon:
return "canyon";
case runtime::TrackEnvironment::Tunnel:
return "tunnel";
case runtime::TrackEnvironment::Bridge:
return "bridge";
case runtime::TrackEnvironment::Bank:
return "bank";
default:
return "flat";
}
}
[[nodiscard]] inline std::string_view trackKindToken(const runtime::RuntimeTrack& track) {
if (track.category == runtime::TrackCategory::Road) {
return "road";
}
if (track.category == runtime::TrackCategory::Water) {
return "river";
}
switch (track.trackType) {
case runtime::TrackType::Switch:
return "switch";
case runtime::TrackType::Cross:
return "cross";
case runtime::TrackType::Table:
return "turn";
default:
return "normal";
}
}
inline void pushTrackCore(TokenWriter& w, const runtime::RuntimeTrack& track) {
w.push(track.length);
w.push(track.trackWidth);
w.push(track.friction);
w.push(track.soundDistance);
w.push(track.qualityFlag);
w.push(track.damageFlag);
w.push(trackEnvironmentName(track.environment));
if (track.visibility) {
w.push("vis");
w.push(track.visibility->material1);
w.push(track.visibility->texLength);
w.push(track.visibility->material2);
w.push(track.visibility->texHeight1);
w.push(track.visibility->texWidth);
w.push(track.visibility->texSlope);
} else {
w.push("unvis");
}
}
inline void pushTrackSegments(
TokenWriter& w,
const runtime::RuntimeTrack& track,
const runtime::TransformContext& transform) {
for (const runtime::SegmentPath& seg : track.paths) {
w.pushVec3(runtime::subtractOriginOffset(seg.pStart, transform));
w.push(seg.rollStart);
w.pushVec3(seg.cpOut);
w.pushVec3(seg.cpIn);
w.pushVec3(runtime::subtractOriginOffset(seg.pEnd, transform));
w.push(seg.rollEnd);
w.push(seg.radius);
}
for (const auto& [key, value] : track.tailKeywords) {
w.push(key);
if (key == "sleepermodel") {
std::size_t start = 0;
while (start < value.size()) {
const std::size_t end = value.find(' ', start);
if (end == std::string::npos) {
w.push(value.substr(start));
break;
}
w.push(value.substr(start, end - start));
start = end + 1;
}
} else {
w.push(value);
}
}
}
inline void emitTrack(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeTrack& track) {
emitNodeTransform(w, state, track.node);
pushNodeHeader(w, track.node);
w.push("track");
w.push(trackKindToken(track));
pushTrackCore(w, track);
pushTrackSegments(w, track, track.node.transform);
w.push("endtrack");
w.newline();
}
[[nodiscard]] inline std::string tractionResistivityToken(const runtime::RuntimeTraction& traction) {
if (traction.resistivityLegacy != 0.0) {
return formatDouble(traction.resistivityLegacy);
}
return formatDouble(traction.resistivityOhmPerM / 0.001);
}
[[nodiscard]] inline std::string tractionMaterialToken(const runtime::RuntimeTraction& traction) {
if (!traction.materialRaw.empty()) {
return traction.materialRaw;
}
switch (traction.material) {
case runtime::TractionWireMaterial::Aluminium:
return "al";
case runtime::TractionWireMaterial::None:
return "none";
default:
return "cu";
}
}
inline void emitTraction(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeTraction& traction) {
emitNodeTransform(w, state, traction.node);
const runtime::TransformContext& transform = traction.node.transform;
pushNodeHeader(w, traction.node);
w.push("traction");
w.push(traction.powerSupplyName);
w.push(traction.nominalVoltage);
w.push(traction.maxCurrent);
w.push(tractionResistivityToken(traction));
w.push(tractionMaterialToken(traction));
w.push(traction.wireThickness);
w.push(traction.damageFlag);
w.pushVec3(runtime::subtractOriginOffset(traction.wireP1, transform));
w.pushVec3(runtime::subtractOriginOffset(traction.wireP2, transform));
w.pushVec3(runtime::subtractOriginOffset(traction.wireP3, transform));
w.pushVec3(runtime::subtractOriginOffset(traction.wireP4, transform));
w.push(traction.minHeight);
w.push(traction.segmentLength);
w.push(traction.wireCount);
w.push(traction.wireOffset);
w.push(traction.node.visible ? "vis" : "unvis");
if (traction.parallelName) {
w.push("parallel");
w.push(*traction.parallelName);
}
w.push("endtraction");
w.newline();
}
inline void emitPowerSource(
TokenWriter& w,
TransformEmitState& state,
const runtime::RuntimeTractionPowerSource& source) {
emitNodeTransform(w, state, source.node);
pushNodeHeader(w, source.node);
w.push("tractionpowersource");
w.pushVec3(runtime::inverseTransformPoint(source.position, source.node.transform));
w.push(source.nominalVoltage);
w.push(source.voltageFrequency);
w.push(source.internalResistanceLegacy);
w.push(source.maxOutputCurrent);
w.push(source.fastFuseTimeout);
w.push(source.fastFuseRepetition);
w.push(source.slowFuseTimeout);
if (source.modifier == runtime::PowerSourceModifier::Recuperation) {
w.push("recuperation");
} else if (source.modifier == runtime::PowerSourceModifier::Section) {
w.push("section");
}
w.push("end");
w.newline();
}
inline void emitShape(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeShapeNode& shape) {
emitNodeTransform(w, state, shape.node);
std::vector<runtime::WorldVertex> localVertices = shape.vertices;
runtime::inverseTransformShapeVertices(localVertices, shape.node.transform);
pushNodeHeader(w, shape.node);
const std::string_view shapeType =
shape.node.nodeType.empty() ? std::string_view{"triangles"} : std::string_view{shape.node.nodeType};
w.push(shapeType);
w.push(shape.materialPath);
const bool triangleList = shapeType == "triangles";
for (std::size_t i = 0; i < localVertices.size(); ++i) {
const runtime::WorldVertex& vert = localVertices[i];
w.pushVec3(vert.position);
w.pushVec3(vert.normal);
w.push(vert.u);
w.push(vert.v);
// MaSzyna shape_node::import: po kazdym wierzcholku getToken() — oczekuje
// "end" po 1. i 2. rogu kazdego trojkata (nie po 3.).
if (triangleList && i % 3 != 2) {
w.push("end");
}
}
w.push("endtri");
w.newline();
}
inline void emitLines(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeLinesNode& lines) {
emitNodeTransform(w, state, lines.node);
std::vector<runtime::WorldVertex> localVertices = lines.vertices;
runtime::inverseTransformShapeVertices(localVertices, lines.node.transform);
pushNodeHeader(w, lines.node);
w.push("lines");
w.push(lines.lighting.diffuse.x * 255.0);
w.push(lines.lighting.diffuse.y * 255.0);
w.push(lines.lighting.diffuse.z * 255.0);
w.push(lines.lineWidth);
for (std::size_t i = 0; i + 1 < localVertices.size(); i += 2) {
w.pushVec3(localVertices[i].position);
w.pushVec3(localVertices[i + 1].position);
}
w.push("endlines");
w.newline();
}
inline void emitModel(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeModelInstance& model) {
emitNodeTransform(w, state, model.node);
const runtime::Vec3 localLocation =
runtime::inverseTransformPoint(model.location, model.node.transform);
const double localRotationY = model.angles.y - model.node.transform.rotation.y;
pushNodeHeader(w, model.node);
w.push("model");
w.pushVec3(localLocation);
w.push(localRotationY);
w.push(model.modelFile);
w.push(model.textureFile);
w.push("endmodel");
w.newline();
}
inline void emitMemcell(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeMemCell& cell) {
emitNodeTransform(w, state, cell.node);
pushNodeHeader(w, cell.node);
w.push("memcell");
w.pushVec3(runtime::inverseTransformPoint(cell.node.area.center, cell.node.transform));
w.push(cell.text);
w.push(cell.value1);
w.push(cell.value2);
w.push(cell.trackName.value_or("none"));
w.push("endmemcell");
w.newline();
}
[[nodiscard]] inline std::string launcherTimeToken(const runtime::RuntimeEventLauncher& launcher) {
if (launcher.launchHour != 0 || launcher.launchMinute != 0) {
return std::format("{}", launcher.launchHour * 100 + launcher.launchMinute);
}
if (launcher.deltaTime != 0.0) {
return formatDouble(-launcher.deltaTime);
}
return "0";
}
inline void emitLauncher(
TokenWriter& w,
TransformEmitState& state,
const runtime::RuntimeEventLauncher& launcher) {
emitNodeTransform(w, state, launcher.node);
pushNodeHeader(w, launcher.node);
w.push("eventlauncher");
w.pushVec3(runtime::inverseTransformPoint(launcher.location, launcher.node.transform));
w.push(std::sqrt(launcher.radiusSquared));
w.push(launcher.activationKeyRaw);
w.push(launcherTimeToken(launcher));
w.push(launcher.event1Name);
if (!launcher.event2Name.empty() && launcher.event2Name != "none") {
w.push(launcher.event2Name);
}
if (launcher.condition) {
w.push("condition");
w.push(launcher.condition->memcellName);
w.push(launcher.condition->compareText);
if (launcher.condition->checkMask & 2) {
w.push(launcher.condition->compareValue1);
} else {
w.push("*");
}
if (launcher.condition->checkMask & 4) {
w.push(launcher.condition->compareValue2);
} else {
w.push("*");
}
}
if (launcher.trainTriggered) {
w.push("traintriggered");
}
w.push("endeventlauncher");
w.newline();
}
inline void emitDynamic(
TokenWriter& w,
TransformEmitState& state,
const runtime::RuntimeDynamicObject& vehicle,
const bool inTrainset) {
emitNodeTransform(w, state, vehicle.node);
pushNodeHeader(w, vehicle.node);
w.push("dynamic");
w.push(vehicle.dataFolder);
w.push(vehicle.skinFile);
w.push(vehicle.mmdFile);
if (!inTrainset) {
w.push(vehicle.trackName);
}
w.push(vehicle.offset);
w.push(vehicle.driverType);
if (inTrainset) {
w.push(vehicle.couplingRaw.empty() ? std::to_string(vehicle.coupling) : vehicle.couplingRaw);
}
if (!inTrainset) {
w.push(vehicle.velocity);
}
w.push(vehicle.loadCount);
if (!vehicle.loadType.empty()) {
w.push(vehicle.loadType);
}
if (vehicle.destination) {
w.push(*vehicle.destination);
}
w.push("enddynamic");
w.newline();
}
inline void emitSound(TokenWriter& w, TransformEmitState& state, const runtime::RuntimeSoundSource& sound) {
emitNodeTransform(w, state, sound.node);
pushNodeHeader(w, sound.node);
w.push("sound");
w.pushVec3(runtime::inverseTransformPoint(sound.location, sound.node.transform));
w.push(sound.wavFile);
w.push("endsound");
w.newline();
}
[[nodiscard]] inline std::string_view eventTypeName(const runtime::EventType type) {
switch (type) {
case runtime::EventType::AddValues:
return "addvalues";
case runtime::EventType::UpdateValues:
return "updatevalues";
case runtime::EventType::CopyValues:
return "copyvalues";
case runtime::EventType::GetValues:
return "getvalues";
case runtime::EventType::PutValues:
return "putvalues";
case runtime::EventType::Whois:
return "whois";
case runtime::EventType::LogValues:
return "logvalues";
case runtime::EventType::Multiple:
return "multiple";
case runtime::EventType::Switch:
return "switch";
case runtime::EventType::TrackVel:
return "trackvel";
case runtime::EventType::Sound:
return "sound";
case runtime::EventType::Texture:
return "texture";
case runtime::EventType::Animation:
return "animation";
case runtime::EventType::Lights:
return "lights";
case runtime::EventType::Voltage:
return "voltage";
case runtime::EventType::Visible:
return "visible";
case runtime::EventType::Friction:
return "friction";
case runtime::EventType::Message:
return "message";
default:
return "unknown";
}
}
[[nodiscard]] inline std::string joinTargets(const std::vector<std::string>& targets) {
std::string joined;
for (std::size_t i = 0; i < targets.size(); ++i) {
if (i > 0) {
joined += '|';
}
joined += targets[i];
}
return joined.empty() ? "none" : joined;
}
inline void emitEvent(TokenWriter& w, const runtime::RuntimeEvent& event) {
w.push("event");
w.push(event.name);
w.push(eventTypeName(event.type));
w.push(event.delay);
w.push(joinTargets(event.targets));
for (const auto& [key, value] : event.payload) {
if (!key.empty()) {
w.push(key);
}
if (!value.empty()) {
w.push(value);
}
}
if (event.delayRandom != 0.0) {
w.push("randomdelay");
w.push(event.delayRandom);
}
if (event.delayDeparture != 0.0) {
w.push("departuredelay");
w.push(event.delayDeparture);
}
if (event.passive) {
w.push("passive");
}
w.push("endevent");
w.newline();
}
inline void emitInclude(TokenWriter& w, const bake::ModuleInclude& include) {
w.push("include");
w.push(includeTextPath(include));
for (const std::string& param : include.parameters) {
w.push(param);
}
w.push("end");
w.newline();
}
inline void emitTrainsetBlock(
TokenWriter& w,
TransformEmitState& state,
const runtime::RuntimeTrainset& trainset,
const std::vector<runtime::RuntimeDynamicObject>& dynamics) {
w.push("trainset");
w.push(trainset.name);
w.push(trainset.track);
w.push(trainset.offset);
w.push(trainset.velocity);
for (const std::size_t index : trainset.vehicleIndices) {
if (index < dynamics.size()) {
emitDynamic(w, state, dynamics[index], true);
}
}
w.push("endtrainset");
w.newline();
}
[[nodiscard]] inline std::vector<SourceToken> runtimeModuleToTokens(const bake::RuntimeModule& module) {
TokenWriter w;
TransformEmitState transformState;
for (const bake::ModuleInclude& include : module.includes) {
emitInclude(w, include);
}
for (const runtime::RuntimeTrack& track : module.scene.tracks) {
emitTrack(w, transformState, track);
}
for (const runtime::RuntimeTraction& traction : module.scene.traction) {
emitTraction(w, transformState, traction);
}
for (const runtime::RuntimeTractionPowerSource& source : module.scene.powerSources) {
emitPowerSource(w, transformState, source);
}
for (const runtime::RuntimeShapeNode& shape : module.scene.shapes) {
emitShape(w, transformState, shape);
}
for (const runtime::RuntimeLinesNode& lines : module.scene.lines) {
emitLines(w, transformState, lines);
}
for (const runtime::RuntimeModelInstance& model : module.scene.models) {
emitModel(w, transformState, model);
}
for (const runtime::RuntimeMemCell& cell : module.scene.memcells) {
emitMemcell(w, transformState, cell);
}
for (const runtime::RuntimeEventLauncher& launcher : module.scene.eventLaunchers) {
emitLauncher(w, transformState, launcher);
}
std::vector<bool> emittedInTrainset(module.scene.dynamics.size(), false);
for (const runtime::RuntimeTrainset& trainset : module.scene.trainsets) {
for (const std::size_t index : trainset.vehicleIndices) {
if (index < emittedInTrainset.size()) {
emittedInTrainset[index] = true;
}
}
}
for (std::size_t i = 0; i < module.scene.dynamics.size(); ++i) {
if (!emittedInTrainset[i]) {
emitDynamic(w, transformState, module.scene.dynamics[i], false);
}
}
for (const runtime::RuntimeSoundSource& sound : module.scene.sounds) {
emitSound(w, transformState, sound);
}
for (const runtime::RuntimeEvent& event : module.scene.events) {
emitEvent(w, event);
}
for (std::uint32_t i = 0; i < module.scene.firstInitCount; ++i) {
w.push("FirstInit");
w.newline();
}
for (const runtime::RuntimeTrainset& trainset : module.scene.trainsets) {
emitTrainsetBlock(w, transformState, trainset, module.scene.dynamics);
}
return w.tokens;
}
[[nodiscard]] inline bool endsDetokenizedLine(std::string_view token) noexcept {
return token == "end" || token == "endtrack" || token == "endtraction" ||
token == "endtri" || token == "endtriangles" || token == "endlines" || token == "endmodel" ||
token == "endmemcell" || token == "endeventlauncher" || token == "enddynamic" ||
token == "endsound" || token == "endtrainset" || token == "endevent" ||
token == "endorigin" || token == "endscale" || token == "endgroup" ||
token == "FirstInit";
}
[[nodiscard]] inline std::string tokensToText(const std::vector<SourceToken>& tokens) {
std::string text;
text.reserve(tokens.size() * 12);
for (std::size_t i = 0; i < tokens.size(); ++i) {
if (i > 0) {
if (tokens[i].sourceLine != tokens[i - 1].sourceLine) {
text.push_back('\n');
} else {
text.push_back(' ');
}
}
text += tokens[i].value;
if (endsDetokenizedLine(tokens[i].value)) {
if (i + 1 >= tokens.size() || tokens[i + 1].sourceLine == tokens[i].sourceLine) {
text.push_back('\n');
}
}
}
return text;
}
} // namespace eu07::scene::binary::detokenize

View File

@@ -0,0 +1,141 @@
#pragma once
#include <eu07/scene/node/types.hpp>
#include <array>
#include <cstdint>
#include <cstring>
#include <istream>
#include <ostream>
#include <stdexcept>
#include <string>
namespace eu07::scene::binary::io {
inline void writeU8(std::ostream& out, const std::uint8_t v) {
out.put(static_cast<char>(v));
}
inline void writeU16(std::ostream& out, const std::uint16_t v) {
const auto b = std::array<std::uint8_t, 2>{
static_cast<std::uint8_t>(v),
static_cast<std::uint8_t>(v >> 8),
};
out.write(reinterpret_cast<const char*>(b.data()), 2);
}
inline void writeU32(std::ostream& out, const std::uint32_t v) {
const auto b = std::array<std::uint8_t, 4>{
static_cast<std::uint8_t>(v),
static_cast<std::uint8_t>(v >> 8),
static_cast<std::uint8_t>(v >> 16),
static_cast<std::uint8_t>(v >> 24),
};
out.write(reinterpret_cast<const char*>(b.data()), 4);
}
inline void writeU64(std::ostream& out, const std::uint64_t v) {
writeU32(out, static_cast<std::uint32_t>(v));
writeU32(out, static_cast<std::uint32_t>(v >> 32));
}
inline void writeI32(std::ostream& out, const std::int32_t v) {
writeU32(out, static_cast<std::uint32_t>(v));
}
inline void writeI16(std::ostream& out, const std::int16_t v) {
writeU16(out, static_cast<std::uint16_t>(v));
}
inline void writeF32(std::ostream& out, const float v) {
static_assert(sizeof(float) == 4);
std::uint32_t bits = 0;
std::memcpy(&bits, &v, 4);
writeU32(out, bits);
}
// Skalary i Vec3 w EU7B v3+ — float32 na dysku (runtime w pamieci nadal double).
inline void writeF64(std::ostream& out, const double v) {
writeF32(out, static_cast<float>(v));
}
inline void writeVec3(std::ostream& out, const Vec3& v) {
writeF64(out, v.x);
writeF64(out, v.y);
writeF64(out, v.z);
}
inline void writeChunkHeader(std::ostream& out, const std::array<char, 4>& id, const std::uint32_t size) {
out.write(id.data(), 4);
writeU32(out, size);
}
[[nodiscard]] inline std::uint8_t readU8(std::istream& in) {
const int ch = in.get();
if (ch == EOF) {
throw std::runtime_error("EU7B: nieoczekiwany koniec pliku");
}
return static_cast<std::uint8_t>(ch);
}
[[nodiscard]] inline std::uint32_t readU32(std::istream& in) {
std::array<std::uint8_t, 4> b{};
in.read(reinterpret_cast<char*>(b.data()), 4);
if (!in) {
throw std::runtime_error("EU7B: nieoczekiwany koniec pliku");
}
return static_cast<std::uint32_t>(b[0]) | (static_cast<std::uint32_t>(b[1]) << 8) |
(static_cast<std::uint32_t>(b[2]) << 16) | (static_cast<std::uint32_t>(b[3]) << 24);
}
[[nodiscard]] inline std::uint64_t readU64(std::istream& in) {
const std::uint64_t lo = readU32(in);
const std::uint64_t hi = readU32(in);
return lo | (hi << 32);
}
[[nodiscard]] inline std::int32_t readI32(std::istream& in) {
return static_cast<std::int32_t>(readU32(in));
}
[[nodiscard]] inline std::uint16_t readU16(std::istream& in) {
std::array<std::uint8_t, 2> b{};
in.read(reinterpret_cast<char*>(b.data()), 2);
if (!in) {
throw std::runtime_error("EU7B: nieoczekiwany koniec pliku");
}
return static_cast<std::uint16_t>(b[0]) | (static_cast<std::uint16_t>(b[1]) << 8);
}
[[nodiscard]] inline std::int16_t readI16(std::istream& in) {
return static_cast<std::int16_t>(readU16(in));
}
[[nodiscard]] inline float readF32(std::istream& in) {
const std::uint32_t bits = readU32(in);
float v = 0.f;
std::memcpy(&v, &bits, 4);
return v;
}
[[nodiscard]] inline double readF64(std::istream& in) {
return static_cast<double>(readF32(in));
}
[[nodiscard]] inline Vec3 readVec3(std::istream& in) {
return {readF64(in), readF64(in), readF64(in)};
}
[[nodiscard]] inline std::string readString(std::istream& in) {
const std::uint32_t len = readU32(in);
std::string text(len, '\0');
if (len > 0) {
in.read(text.data(), static_cast<std::streamsize>(len));
if (!in) {
throw std::runtime_error("EU7B: nieoczekiwany koniec pliku (string)");
}
}
return text;
}
} // namespace eu07::scene::binary::io

View File

@@ -0,0 +1,165 @@
#pragma once
// EU7B v2 — chunki TRSET, EVNT (include wewnątrz binary::detail).
#include <eu07/scene/binary/io.hpp>
#include <eu07/scene/binary/runtime_codec.hpp>
#include <eu07/scene/binary/string_table.hpp>
#include <eu07/scene/bake/module.hpp>
#include <eu07/scene/runtime/directives.hpp>
#include <istream>
#include <ostream>
#include <sstream>
#include <vector>
inline void writeRuntimeTrainset(
std::ostream& out,
StringTable& table,
const runtime::RuntimeTrainset& trainset) {
codec::writeStringId(out, table, trainset.name);
codec::writeStringId(out, table, trainset.track);
io::writeF32(out, trainset.offset);
io::writeF32(out, trainset.velocity);
io::writeU32(out, static_cast<std::uint32_t>(trainset.assignment.size()));
for (const auto& [key, value] : trainset.assignment) {
codec::writeStringId(out, table, key);
codec::writeStringId(out, table, value);
}
io::writeU32(out, static_cast<std::uint32_t>(trainset.vehicleIndices.size()));
for (const std::size_t index : trainset.vehicleIndices) {
io::writeU32(out, static_cast<std::uint32_t>(index));
}
io::writeU32(out, static_cast<std::uint32_t>(trainset.couplings.size()));
for (const int coupling : trainset.couplings) {
io::writeI32(out, coupling);
}
io::writeU32(out, static_cast<std::uint32_t>(trainset.driverIndex));
}
inline void writeRuntimeEvent(std::ostream& out, StringTable& table, const runtime::RuntimeEvent& event) {
codec::writeStringId(out, table, event.name);
io::writeU8(out, static_cast<std::uint8_t>(event.type));
io::writeF64(out, event.delay);
io::writeU32(out, static_cast<std::uint32_t>(event.targets.size()));
for (const std::string& target : event.targets) {
codec::writeStringId(out, table, target);
}
io::writeF64(out, event.delayRandom);
io::writeF64(out, event.delayDeparture);
io::writeU8(out, event.ignored ? 1 : 0);
io::writeU8(out, event.passive ? 1 : 0);
io::writeU32(out, static_cast<std::uint32_t>(event.payload.size()));
for (const auto& [key, value] : event.payload) {
codec::writeStringId(out, table, key);
codec::writeStringId(out, table, value);
}
}
inline void collectTrainsetStrings(StringTable& table, const runtime::RuntimeTrainset& trainset) {
table.intern(trainset.name);
table.intern(trainset.track);
for (const auto& [key, value] : trainset.assignment) {
table.intern(key);
table.intern(value);
}
}
inline void collectEventStrings(StringTable& table, const runtime::RuntimeEvent& event) {
table.intern(event.name);
for (const std::string& target : event.targets) {
table.intern(target);
}
for (const auto& [key, value] : event.payload) {
table.intern(key);
table.intern(value);
}
}
[[nodiscard]] inline std::vector<char> buildTrsetPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeTrainset>& trainsets) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(trainsets.size()));
for (const runtime::RuntimeTrainset& trainset : trainsets) {
writeRuntimeTrainset(out, table, trainset);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
[[nodiscard]] inline std::vector<char> buildEvntPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeEvent>& events) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(events.size()));
for (const runtime::RuntimeEvent& event : events) {
writeRuntimeEvent(out, table, event);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
inline runtime::RuntimeTrainset readRuntimeTrainset(std::istream& in, const StringTable& table) {
runtime::RuntimeTrainset trainset;
trainset.name = table.resolve(io::readU32(in));
trainset.track = table.resolve(io::readU32(in));
trainset.offset = io::readF32(in);
trainset.velocity = io::readF32(in);
const std::uint32_t assignmentCount = io::readU32(in);
for (std::uint32_t i = 0; i < assignmentCount; ++i) {
const std::string key = table.resolve(io::readU32(in));
const std::string value = table.resolve(io::readU32(in));
trainset.assignment.emplace(std::move(key), std::move(value));
}
const std::uint32_t vehicleCount = io::readU32(in);
trainset.vehicleIndices.reserve(vehicleCount);
for (std::uint32_t i = 0; i < vehicleCount; ++i) {
trainset.vehicleIndices.push_back(io::readU32(in));
}
const std::uint32_t couplingCount = io::readU32(in);
trainset.couplings.reserve(couplingCount);
for (std::uint32_t i = 0; i < couplingCount; ++i) {
trainset.couplings.push_back(io::readI32(in));
}
trainset.driverIndex = io::readU32(in);
return trainset;
}
inline runtime::RuntimeEvent readRuntimeEvent(std::istream& in, const StringTable& table) {
runtime::RuntimeEvent event;
event.name = table.resolve(io::readU32(in));
event.type = static_cast<runtime::EventType>(io::readU8(in));
event.delay = io::readF64(in);
const std::uint32_t targetCount = io::readU32(in);
event.targets.reserve(targetCount);
for (std::uint32_t i = 0; i < targetCount; ++i) {
event.targets.push_back(table.resolve(io::readU32(in)));
}
event.delayRandom = io::readF64(in);
event.delayDeparture = io::readF64(in);
event.ignored = io::readU8(in) != 0;
event.passive = io::readU8(in) != 0;
const std::uint32_t payloadCount = io::readU32(in);
event.payload.reserve(payloadCount);
for (std::uint32_t i = 0; i < payloadCount; ++i) {
event.payload.emplace_back(table.resolve(io::readU32(in)), table.resolve(io::readU32(in)));
}
return event;
}
inline void readTrsetChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.trainsets.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.trainsets.push_back(readRuntimeTrainset(in, table));
}
}
inline void readEvntChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.events.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.events.push_back(readRuntimeEvent(in, table));
}
}

View File

@@ -0,0 +1,429 @@
#pragma once
// EU7B v2 — chunki TRAC, PWRS, MEMC, LAUN, DYNM, SOND (include wewnątrz binary::detail).
#include <eu07/scene/binary/io.hpp>
#include <eu07/scene/binary/runtime_codec.hpp>
#include <eu07/scene/binary/string_table.hpp>
#include <eu07/scene/bake/module.hpp>
#include <istream>
#include <ostream>
#include <sstream>
#include <vector>
inline void writeRuntimeTraction(
std::ostream& out,
StringTable& table,
const runtime::RuntimeTraction& traction) {
codec::writeSlimNode(out, table, traction.node, "traction");
codec::writeStringId(out, table, traction.powerSupplyName);
io::writeU8(out, static_cast<std::uint8_t>(traction.material));
io::writeF32(out, traction.nominalVoltage);
io::writeF32(out, traction.maxCurrent);
io::writeF32(out, traction.resistivityOhmPerM);
io::writeF64(out, traction.resistivityLegacy);
codec::writeStringId(out, table, traction.materialRaw);
io::writeF32(out, traction.wireThickness);
io::writeI32(out, traction.damageFlag);
io::writeVec3(out, traction.wireP1);
io::writeVec3(out, traction.wireP2);
io::writeVec3(out, traction.wireP3);
io::writeVec3(out, traction.wireP4);
io::writeF64(out, traction.minHeight);
io::writeF64(out, traction.segmentLength);
io::writeI32(out, traction.wireCount);
io::writeF32(out, traction.wireOffset);
io::writeU8(out, traction.parallelName.has_value() ? 1 : 0);
if (traction.parallelName) {
codec::writeStringId(out, table, *traction.parallelName);
}
}
inline void writeRuntimeMemcell(
std::ostream& out,
StringTable& table,
const runtime::RuntimeMemCell& cell) {
codec::writeSlimNode(out, table, cell.node, "memcell");
codec::writeStringId(out, table, cell.text);
io::writeF64(out, cell.value1);
io::writeF64(out, cell.value2);
codec::writeStringId(out, table, cell.trackName.value_or(""));
}
inline void writeRuntimeLauncher(
std::ostream& out,
StringTable& table,
const runtime::RuntimeEventLauncher& launcher) {
codec::writeSlimNode(out, table, launcher.node, "eventlauncher");
io::writeVec3(out, launcher.location);
io::writeF64(out, launcher.radiusSquared);
codec::writeStringId(out, table, launcher.activationKeyRaw);
io::writeI32(out, launcher.activationKey);
io::writeF64(out, launcher.deltaTime);
codec::writeStringId(out, table, launcher.event1Name);
codec::writeStringId(out, table, launcher.event2Name);
io::writeI32(out, launcher.launchHour);
io::writeI32(out, launcher.launchMinute);
io::writeU8(out, launcher.condition.has_value() ? 1 : 0);
if (launcher.condition) {
codec::writeStringId(out, table, launcher.condition->memcellName);
codec::writeStringId(out, table, launcher.condition->compareText);
io::writeF64(out, launcher.condition->compareValue1);
io::writeF64(out, launcher.condition->compareValue2);
io::writeI32(out, launcher.condition->checkMask);
}
io::writeU8(out, launcher.trainTriggered ? 1 : 0);
}
inline void writeRuntimeDynamic(
std::ostream& out,
StringTable& table,
const runtime::RuntimeDynamicObject& vehicle) {
codec::writeSlimNode(out, table, vehicle.node, "dynamic");
codec::writeStringId(out, table, vehicle.dataFolder);
codec::writeStringId(out, table, vehicle.skinFile);
codec::writeStringId(out, table, vehicle.mmdFile);
codec::writeStringId(out, table, vehicle.trackName);
codec::writeStringId(out, table, vehicle.driverType);
codec::writeStringId(out, table, vehicle.loadType);
codec::writeStringId(out, table, vehicle.couplingParams);
codec::writeStringId(out, table, vehicle.couplingRaw);
io::writeF64(out, vehicle.offset);
io::writeI32(out, vehicle.coupling);
io::writeI32(out, vehicle.loadCount);
io::writeF32(out, vehicle.velocity);
io::writeU8(out, vehicle.destination.has_value() ? 1 : 0);
if (vehicle.destination) {
codec::writeStringId(out, table, *vehicle.destination);
}
io::writeU8(out, vehicle.trainsetIndex.has_value() ? 1 : 0);
if (vehicle.trainsetIndex) {
io::writeU32(out, static_cast<std::uint32_t>(*vehicle.trainsetIndex));
}
}
inline void writeRuntimeSound(
std::ostream& out,
StringTable& table,
const runtime::RuntimeSoundSource& sound) {
codec::writeSlimNode(out, table, sound.node, "sound");
io::writeVec3(out, sound.location);
codec::writeStringId(out, table, sound.wavFile);
}
inline void collectTractionStrings(StringTable& table, const runtime::RuntimeTraction& traction) {
table.intern(traction.node.name);
table.intern(traction.powerSupplyName);
table.intern(traction.materialRaw);
if (traction.parallelName) {
table.intern(*traction.parallelName);
}
}
inline void collectMemcellStrings(StringTable& table, const runtime::RuntimeMemCell& cell) {
table.intern(cell.node.name);
table.intern(cell.text);
if (cell.trackName) {
table.intern(*cell.trackName);
}
}
inline void collectLauncherStrings(StringTable& table, const runtime::RuntimeEventLauncher& launcher) {
table.intern(launcher.node.name);
table.intern(launcher.activationKeyRaw);
table.intern(launcher.event1Name);
table.intern(launcher.event2Name);
if (launcher.condition) {
table.intern(launcher.condition->memcellName);
table.intern(launcher.condition->compareText);
}
}
inline void collectDynamicStrings(StringTable& table, const runtime::RuntimeDynamicObject& vehicle) {
table.intern(vehicle.node.name);
table.intern(vehicle.dataFolder);
table.intern(vehicle.skinFile);
table.intern(vehicle.mmdFile);
table.intern(vehicle.trackName);
table.intern(vehicle.driverType);
table.intern(vehicle.loadType);
table.intern(vehicle.couplingParams);
table.intern(vehicle.couplingRaw);
if (vehicle.destination) {
table.intern(*vehicle.destination);
}
}
inline void collectSoundStrings(StringTable& table, const runtime::RuntimeSoundSource& sound) {
table.intern(sound.node.name);
table.intern(sound.wavFile);
}
[[nodiscard]] inline std::vector<char> buildTracPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeTraction>& traction) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(traction.size()));
for (const runtime::RuntimeTraction& item : traction) {
writeRuntimeTraction(out, table, item);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
[[nodiscard]] inline std::vector<char> buildMemcPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeMemCell>& cells) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(cells.size()));
for (const runtime::RuntimeMemCell& cell : cells) {
writeRuntimeMemcell(out, table, cell);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
[[nodiscard]] inline std::vector<char> buildLaunPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeEventLauncher>& launchers) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(launchers.size()));
for (const runtime::RuntimeEventLauncher& launcher : launchers) {
writeRuntimeLauncher(out, table, launcher);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
[[nodiscard]] inline std::vector<char> buildDynmPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeDynamicObject>& dynamics) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(dynamics.size()));
for (const runtime::RuntimeDynamicObject& vehicle : dynamics) {
writeRuntimeDynamic(out, table, vehicle);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
[[nodiscard]] inline std::vector<char> buildSondPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeSoundSource>& sounds) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(sounds.size()));
for (const runtime::RuntimeSoundSource& sound : sounds) {
writeRuntimeSound(out, table, sound);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
inline runtime::RuntimeTraction readRuntimeTraction(std::istream& in, const StringTable& table) {
runtime::RuntimeTraction traction;
traction.node = codec::readSlimNode(in, table, "traction");
traction.powerSupplyName = table.resolve(io::readU32(in));
traction.material = static_cast<runtime::TractionWireMaterial>(io::readU8(in));
traction.nominalVoltage = io::readF32(in);
traction.maxCurrent = io::readF32(in);
traction.resistivityOhmPerM = io::readF32(in);
traction.resistivityLegacy = io::readF64(in);
traction.materialRaw = table.resolve(io::readU32(in));
traction.wireThickness = io::readF32(in);
traction.damageFlag = io::readI32(in);
traction.wireP1 = io::readVec3(in);
traction.wireP2 = io::readVec3(in);
traction.wireP3 = io::readVec3(in);
traction.wireP4 = io::readVec3(in);
traction.minHeight = io::readF64(in);
traction.segmentLength = io::readF64(in);
traction.wireCount = io::readI32(in);
traction.wireOffset = io::readF32(in);
if (io::readU8(in) != 0) {
const std::string parallel = table.resolve(io::readU32(in));
if (!parallel.empty()) {
traction.parallelName = parallel;
}
}
return traction;
}
inline runtime::RuntimeMemCell readRuntimeMemcell(std::istream& in, const StringTable& table) {
runtime::RuntimeMemCell cell;
cell.node = codec::readSlimNode(in, table, "memcell");
cell.text = table.resolve(io::readU32(in));
cell.value1 = io::readF64(in);
cell.value2 = io::readF64(in);
const std::string track = table.resolve(io::readU32(in));
if (!track.empty()) {
cell.trackName = track;
}
return cell;
}
inline runtime::RuntimeEventLauncher readRuntimeLauncher(std::istream& in, const StringTable& table) {
runtime::RuntimeEventLauncher launcher;
launcher.node = codec::readSlimNode(in, table, "eventlauncher");
launcher.location = io::readVec3(in);
launcher.radiusSquared = io::readF64(in);
launcher.activationKeyRaw = table.resolve(io::readU32(in));
launcher.activationKey = io::readI32(in);
launcher.deltaTime = io::readF64(in);
launcher.event1Name = table.resolve(io::readU32(in));
launcher.event2Name = table.resolve(io::readU32(in));
launcher.launchHour = io::readI32(in);
launcher.launchMinute = io::readI32(in);
if (io::readU8(in) != 0) {
runtime::EventLauncherCondition cond;
cond.memcellName = table.resolve(io::readU32(in));
cond.compareText = table.resolve(io::readU32(in));
cond.compareValue1 = io::readF64(in);
cond.compareValue2 = io::readF64(in);
cond.checkMask = io::readI32(in);
launcher.condition = cond;
}
launcher.trainTriggered = io::readU8(in) != 0;
return launcher;
}
inline runtime::RuntimeDynamicObject readRuntimeDynamic(std::istream& in, const StringTable& table) {
runtime::RuntimeDynamicObject vehicle;
vehicle.node = codec::readSlimNode(in, table, "dynamic");
vehicle.dataFolder = table.resolve(io::readU32(in));
vehicle.skinFile = table.resolve(io::readU32(in));
vehicle.mmdFile = table.resolve(io::readU32(in));
vehicle.trackName = table.resolve(io::readU32(in));
vehicle.driverType = table.resolve(io::readU32(in));
vehicle.loadType = table.resolve(io::readU32(in));
vehicle.couplingParams = table.resolve(io::readU32(in));
vehicle.couplingRaw = table.resolve(io::readU32(in));
vehicle.offset = io::readF64(in);
vehicle.coupling = io::readI32(in);
if (vehicle.couplingRaw.empty()) {
if (!vehicle.couplingParams.empty()) {
vehicle.couplingRaw = std::to_string(vehicle.coupling) + "." + vehicle.couplingParams;
} else {
vehicle.couplingRaw = std::to_string(vehicle.coupling);
}
}
vehicle.loadCount = io::readI32(in);
vehicle.velocity = io::readF32(in);
if (io::readU8(in) != 0) {
vehicle.destination = table.resolve(io::readU32(in));
}
if (io::readU8(in) != 0) {
vehicle.trainsetIndex = io::readU32(in);
}
return vehicle;
}
inline runtime::RuntimeSoundSource readRuntimeSound(std::istream& in, const StringTable& table) {
runtime::RuntimeSoundSource sound;
sound.node = codec::readSlimNode(in, table, "sound");
sound.location = io::readVec3(in);
sound.wavFile = table.resolve(io::readU32(in));
return sound;
}
inline void readTracChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.traction.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.traction.push_back(readRuntimeTraction(in, table));
}
}
inline void readMemcChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.memcells.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.memcells.push_back(readRuntimeMemcell(in, table));
}
}
inline void readLaunChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.eventLaunchers.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.eventLaunchers.push_back(readRuntimeLauncher(in, table));
}
}
inline void readDynmChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.dynamics.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.dynamics.push_back(readRuntimeDynamic(in, table));
}
}
inline void readSondChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.sounds.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.sounds.push_back(readRuntimeSound(in, table));
}
}
inline void writeRuntimePowerSource(
std::ostream& out,
StringTable& table,
const runtime::RuntimeTractionPowerSource& source) {
codec::writeSlimNode(out, table, source.node, "tractionpowersource");
io::writeVec3(out, source.position);
io::writeF32(out, source.nominalVoltage);
io::writeF32(out, source.voltageFrequency);
io::writeF64(out, source.internalResistanceLegacy);
io::writeF32(out, source.internalResistance);
io::writeF32(out, source.maxOutputCurrent);
io::writeF32(out, source.fastFuseTimeout);
io::writeF32(out, source.fastFuseRepetition);
io::writeF32(out, source.slowFuseTimeout);
io::writeU8(out, static_cast<std::uint8_t>(source.modifier));
}
inline void collectPowerSourceStrings(
StringTable& table,
const runtime::RuntimeTractionPowerSource& source) {
table.intern(source.node.name);
}
[[nodiscard]] inline std::vector<char> buildPwrsPayloadV2(
StringTable& table,
const std::vector<runtime::RuntimeTractionPowerSource>& sources) {
std::ostringstream out(std::ios::binary);
io::writeU32(out, static_cast<std::uint32_t>(sources.size()));
for (const runtime::RuntimeTractionPowerSource& source : sources) {
writeRuntimePowerSource(out, table, source);
}
const std::string blob = out.str();
return {blob.begin(), blob.end()};
}
inline runtime::RuntimeTractionPowerSource readRuntimePowerSource(
std::istream& in,
const StringTable& table) {
runtime::RuntimeTractionPowerSource source;
source.node = codec::readSlimNode(in, table, "tractionpowersource");
source.position = io::readVec3(in);
source.node.area.center = source.position;
source.nominalVoltage = io::readF32(in);
source.voltageFrequency = io::readF32(in);
source.internalResistanceLegacy = io::readF64(in);
source.internalResistance = io::readF32(in);
source.maxOutputCurrent = io::readF32(in);
source.fastFuseTimeout = io::readF32(in);
source.fastFuseRepetition = io::readF32(in);
source.slowFuseTimeout = io::readF32(in);
source.modifier = static_cast<runtime::PowerSourceModifier>(io::readU8(in));
return source;
}
inline void readPwrsChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) {
const std::uint32_t count = io::readU32(in);
module.scene.powerSources.reserve(count);
for (std::uint32_t i = 0; i < count; ++i) {
module.scene.powerSources.push_back(readRuntimePowerSource(in, table));
}
}

View File

@@ -0,0 +1,737 @@
#pragma once
// EU7B v5 — slim node, TERR terrain-lite, packed vertices, track tail enums.
#include <eu07/scene/binary/io.hpp>
#include <eu07/scene/binary/string_table.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/runtime/basic_node.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <eu07/scene/runtime/types.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <cstdint>
#include <limits>
#include <istream>
#include <ostream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace eu07::scene::binary::codec {
namespace detail {
inline constexpr float kLightingEps = 1e-4f;
inline constexpr std::uint8_t kNodeFlagHasName = 1u << 0;
inline constexpr std::uint8_t kNodeFlagHasRangeMin = 1u << 1;
inline constexpr std::uint8_t kNodeFlagHasRangeMax = 1u << 2;
inline constexpr std::uint8_t kNodeFlagHasBounds = 1u << 3;
inline constexpr std::uint8_t kNodeFlagHasGroup = 1u << 4;
inline constexpr std::uint8_t kNodeFlagHasTransform = 1u << 5;
inline constexpr std::uint8_t kNodeFlagNotVisible = 1u << 6;
inline constexpr std::uint8_t kTrackTailCustom = 255;
inline bool hasNonDefaultRangeMax(const double rangeSquaredMax) noexcept {
return rangeSquaredMax < std::numeric_limits<double>::max();
}
inline bool hasNonEmptyTransform(const runtime::TransformContext& transform) noexcept {
if (!transform.originStack.empty() || !transform.scaleStack.empty() || transform.groupStackDepth != 0) {
return true;
}
return transform.rotation.x != 0.0 || transform.rotation.y != 0.0 || transform.rotation.z != 0.0;
}
inline void writeTransformContext(std::ostream& out, const runtime::TransformContext& transform) {
if (transform.originStack.size() > 255 || transform.scaleStack.size() > 255) {
throw std::runtime_error("EU7B: zbyt gleboki stos transformacji");
}
io::writeU8(out, static_cast<std::uint8_t>(transform.originStack.size()));
for (const runtime::Vec3& offset : transform.originStack) {
io::writeVec3(out, offset);
}
io::writeU8(out, static_cast<std::uint8_t>(transform.scaleStack.size()));
for (const runtime::Vec3& scale : transform.scaleStack) {
io::writeVec3(out, scale);
}
io::writeVec3(out, transform.rotation);
io::writeU8(out, static_cast<std::uint8_t>(transform.groupStackDepth));
}
inline runtime::TransformContext readTransformContext(std::istream& in) {
runtime::TransformContext transform;
const std::uint8_t originCount = io::readU8(in);
transform.originStack.reserve(originCount);
for (std::uint8_t i = 0; i < originCount; ++i) {
transform.originStack.push_back(io::readVec3(in));
}
const std::uint8_t scaleCount = io::readU8(in);
transform.scaleStack.reserve(scaleCount);
for (std::uint8_t i = 0; i < scaleCount; ++i) {
transform.scaleStack.push_back(io::readVec3(in));
}
transform.rotation = io::readVec3(in);
transform.groupStackDepth = io::readU8(in);
return transform;
}
inline void writeStringId(std::ostream& out, StringTable& table, const std::string& text) {
io::writeU32(out, table.intern(text));
}
inline std::int16_t floatToSnorm16(const float value) {
const float clamped = std::clamp(value, -1.f, 1.f);
return static_cast<std::int16_t>(std::lround(clamped * 32767.f));
}
inline float snorm16ToFloat(const std::int16_t value) {
return static_cast<float>(value) * (1.f / 32767.f);
}
inline std::uint16_t floatToHalf(const float value) {
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
const std::uint32_t fExp = (bits >> 23u) & 0xFFu;
std::uint32_t mantissa = bits & 0x7FFFFFu;
if (fExp == 0xFFu) {
return static_cast<std::uint16_t>(sign | 0x7C00u | (mantissa ? 0x0200u : 0u));
}
if (fExp == 0) {
return static_cast<std::uint16_t>(sign);
}
std::int32_t halfExp = static_cast<std::int32_t>(fExp) - 127 + 15;
if (halfExp >= 31) {
return static_cast<std::uint16_t>(sign | 0x7C00u);
}
if (halfExp <= 0) {
if (halfExp < -10) {
return static_cast<std::uint16_t>(sign);
}
mantissa = (mantissa | 0x800000u) >> static_cast<std::uint32_t>(1 - halfExp);
return static_cast<std::uint16_t>(sign | (mantissa >> 13u));
}
return static_cast<std::uint16_t>(
sign | (static_cast<std::uint32_t>(halfExp) << 10u) | ((mantissa + 0x1000u) >> 13u));
}
inline float halfToFloat(const std::uint16_t value) {
const std::uint32_t sign = static_cast<std::uint32_t>(value & 0x8000u) << 16u;
const std::uint32_t exponent = (value & 0x7C00u) >> 10u;
std::uint32_t mantissa = value & 0x03FFu;
std::uint32_t bits = 0;
if (exponent == 0) {
if (mantissa == 0) {
bits = sign;
} else {
std::uint32_t exp = 127 - 15 - 1;
while ((mantissa & 0x400u) == 0) {
mantissa <<= 1u;
--exp;
}
mantissa &= 0x3FFu;
bits = sign | (exp << 23u) | (mantissa << 13u);
}
} else if (exponent == 31) {
bits = sign | 0x7F800000u | (mantissa << 13u);
} else {
bits = sign | ((exponent + 127 - 15) << 23u) | (mantissa << 13u);
}
float result = 0.f;
std::memcpy(&result, &bits, sizeof(result));
return result;
}
inline void writeHalf(std::ostream& out, const float value) {
io::writeU16(out, floatToHalf(value));
}
inline float readHalf(std::istream& in) {
return halfToFloat(io::readU16(in));
}
} // namespace detail
inline void writeStringId(std::ostream& out, StringTable& table, const std::string& text) {
detail::writeStringId(out, table, text);
}
inline void writeSlimNode(
std::ostream& out,
StringTable& table,
const runtime::BasicNode& node,
const std::string_view impliedType) {
std::uint8_t flags = 0;
if (!node.name.empty()) {
flags |= detail::kNodeFlagHasName;
}
if (node.rangeSquaredMin != 0.0) {
flags |= detail::kNodeFlagHasRangeMin;
}
if (detail::hasNonDefaultRangeMax(node.rangeSquaredMax)) {
flags |= detail::kNodeFlagHasRangeMax;
}
if (node.area.radius >= 0.f) {
flags |= detail::kNodeFlagHasBounds;
}
if (node.groupValid) {
flags |= detail::kNodeFlagHasGroup;
}
if (detail::hasNonEmptyTransform(node.transform)) {
flags |= detail::kNodeFlagHasTransform;
}
if (!node.visible) {
flags |= detail::kNodeFlagNotVisible;
}
io::writeU8(out, flags);
if ((flags & detail::kNodeFlagHasName) != 0) {
detail::writeStringId(out, table, node.name);
}
if ((flags & detail::kNodeFlagHasRangeMin) != 0) {
io::writeF64(out, node.rangeSquaredMin);
}
if ((flags & detail::kNodeFlagHasRangeMax) != 0) {
io::writeF64(out, node.rangeSquaredMax);
}
if ((flags & detail::kNodeFlagHasBounds) != 0) {
io::writeVec3(out, node.area.center);
io::writeF32(out, node.area.radius);
}
if ((flags & detail::kNodeFlagHasGroup) != 0) {
io::writeU32(out, static_cast<std::uint32_t>(node.groupHandle));
}
if ((flags & detail::kNodeFlagHasTransform) != 0) {
detail::writeTransformContext(out, node.transform);
}
(void)impliedType;
}
inline runtime::BasicNode readSlimNode(std::istream& in, const StringTable& table, const std::string_view impliedType) {
runtime::BasicNode node;
node.nodeType = std::string(impliedType);
const std::uint8_t flags = io::readU8(in);
if ((flags & detail::kNodeFlagHasName) != 0) {
node.name = table.resolve(io::readU32(in));
}
if ((flags & detail::kNodeFlagHasRangeMin) != 0) {
node.rangeSquaredMin = io::readF64(in);
}
if ((flags & detail::kNodeFlagHasRangeMax) != 0) {
node.rangeSquaredMax = io::readF64(in);
} else {
node.rangeSquaredMax = std::numeric_limits<double>::max();
}
if ((flags & detail::kNodeFlagHasBounds) != 0) {
node.area.center = io::readVec3(in);
node.area.radius = io::readF32(in);
}
if ((flags & detail::kNodeFlagHasGroup) != 0) {
node.groupValid = true;
node.groupHandle = io::readU32(in);
}
if ((flags & detail::kNodeFlagHasTransform) != 0) {
node.transform = detail::readTransformContext(in);
}
node.visible = (flags & detail::kNodeFlagNotVisible) == 0;
return node;
}
[[nodiscard]] inline bool isDefaultLighting(const runtime::LightingData& lighting) noexcept {
const auto near = [&](const float a, const float b) noexcept {
return std::abs(a - b) <= detail::kLightingEps;
};
return near(lighting.diffuse.x, 0.8f) && near(lighting.diffuse.y, 0.8f) &&
near(lighting.diffuse.z, 0.8f) && near(lighting.diffuse.w, 1.f) &&
near(lighting.ambient.x, 0.2f) && near(lighting.ambient.y, 0.2f) &&
near(lighting.ambient.z, 0.2f) && near(lighting.ambient.w, 1.f) &&
near(lighting.specular.x, 0.f) && near(lighting.specular.y, 0.f) &&
near(lighting.specular.z, 0.f) && near(lighting.specular.w, 1.f);
}
inline void writeLightingBlock(std::ostream& out, const runtime::LightingData& lighting) {
io::writeF32(out, lighting.diffuse.x);
io::writeF32(out, lighting.diffuse.y);
io::writeF32(out, lighting.diffuse.z);
io::writeF32(out, lighting.diffuse.w);
io::writeF32(out, lighting.ambient.x);
io::writeF32(out, lighting.ambient.y);
io::writeF32(out, lighting.ambient.z);
io::writeF32(out, lighting.ambient.w);
io::writeF32(out, lighting.specular.x);
io::writeF32(out, lighting.specular.y);
io::writeF32(out, lighting.specular.z);
io::writeF32(out, lighting.specular.w);
}
inline runtime::LightingData readLightingBlock(std::istream& in) {
runtime::LightingData lighting;
lighting.diffuse.x = io::readF32(in);
lighting.diffuse.y = io::readF32(in);
lighting.diffuse.z = io::readF32(in);
lighting.diffuse.w = io::readF32(in);
lighting.ambient.x = io::readF32(in);
lighting.ambient.y = io::readF32(in);
lighting.ambient.z = io::readF32(in);
lighting.ambient.w = io::readF32(in);
lighting.specular.x = io::readF32(in);
lighting.specular.y = io::readF32(in);
lighting.specular.z = io::readF32(in);
lighting.specular.w = io::readF32(in);
return lighting;
}
inline void writeLightingTagged(std::ostream& out, const runtime::LightingData& lighting) {
io::writeU8(out, isDefaultLighting(lighting) ? 0 : 1);
if (!isDefaultLighting(lighting)) {
writeLightingBlock(out, lighting);
}
}
inline runtime::LightingData readLightingTagged(std::istream& in) {
if (io::readU8(in) == 0) {
return runtime::LightingData{};
}
return readLightingBlock(in);
}
inline void writePackedWorldVertex(std::ostream& out, const runtime::WorldVertex& vertex) {
io::writeVec3(out, vertex.position);
io::writeI16(out, detail::floatToSnorm16(static_cast<float>(vertex.normal.x)));
io::writeI16(out, detail::floatToSnorm16(static_cast<float>(vertex.normal.y)));
io::writeI16(out, detail::floatToSnorm16(static_cast<float>(vertex.normal.z)));
detail::writeHalf(out, static_cast<float>(vertex.u));
detail::writeHalf(out, static_cast<float>(vertex.v));
}
inline runtime::WorldVertex readPackedWorldVertex(std::istream& in) {
runtime::WorldVertex vertex;
vertex.position = io::readVec3(in);
vertex.normal.x = detail::snorm16ToFloat(io::readI16(in));
vertex.normal.y = detail::snorm16ToFloat(io::readI16(in));
vertex.normal.z = detail::snorm16ToFloat(io::readI16(in));
vertex.u = detail::readHalf(in);
vertex.v = detail::readHalf(in);
return vertex;
}
[[nodiscard]] inline std::uint8_t trackTailKeywordCode(const std::string_view key) noexcept {
if (isKeyword(key, "event0")) {
return 1;
}
if (isKeyword(key, "eventall0")) {
return 2;
}
if (isKeyword(key, "event1")) {
return 3;
}
if (isKeyword(key, "eventall1")) {
return 4;
}
if (isKeyword(key, "event2")) {
return 5;
}
if (isKeyword(key, "eventall2")) {
return 6;
}
if (isKeyword(key, "velocity")) {
return 7;
}
if (isKeyword(key, "isolated")) {
return 8;
}
if (isKeyword(key, "overhead")) {
return 9;
}
if (isKeyword(key, "vradius")) {
return 10;
}
if (isKeyword(key, "railprofile")) {
return 11;
}
if (isKeyword(key, "trackbed")) {
return 12;
}
if (isKeyword(key, "friction")) {
return 13;
}
if (isKeyword(key, "fouling1")) {
return 14;
}
if (isKeyword(key, "fouling2")) {
return 15;
}
if (isKeyword(key, "sleepermodel")) {
return 16;
}
if (isKeyword(key, "angle1")) {
return 17;
}
if (isKeyword(key, "angle2")) {
return 18;
}
return detail::kTrackTailCustom;
}
[[nodiscard]] inline std::string trackTailKeywordName(const std::uint8_t code) {
switch (code) {
case 1:
return "event0";
case 2:
return "eventall0";
case 3:
return "event1";
case 4:
return "eventall1";
case 5:
return "event2";
case 6:
return "eventall2";
case 7:
return "velocity";
case 8:
return "isolated";
case 9:
return "overhead";
case 10:
return "vradius";
case 11:
return "railprofile";
case 12:
return "trackbed";
case 13:
return "friction";
case 14:
return "fouling1";
case 15:
return "fouling2";
case 16:
return "sleepermodel";
case 17:
return "angle1";
case 18:
return "angle2";
default:
return {};
}
}
inline void writeTrackTailEntry(
std::ostream& out,
StringTable& table,
const std::string& key,
const std::string& value) {
const std::uint8_t code = trackTailKeywordCode(key);
io::writeU8(out, code);
if (code == detail::kTrackTailCustom) {
detail::writeStringId(out, table, key);
}
detail::writeStringId(out, table, value);
}
[[nodiscard]] inline std::uint8_t meshSubtypeCode(const std::string_view type) noexcept {
if (isKeyword(type, "triangle_strip")) {
return 1;
}
if (isKeyword(type, "triangle_fan")) {
return 2;
}
return 0;
}
[[nodiscard]] inline std::string_view meshSubtypeName(const std::uint8_t code) noexcept {
switch (code) {
case 1:
return "triangle_strip";
case 2:
return "triangle_fan";
default:
return "triangles";
}
}
[[nodiscard]] inline std::uint8_t lineSubtypeCode(const std::string_view type) noexcept {
if (isKeyword(type, "line_strip")) {
return 1;
}
if (isKeyword(type, "line_loop")) {
return 2;
}
return 0;
}
[[nodiscard]] inline std::string_view lineSubtypeName(const std::uint8_t code) noexcept {
switch (code) {
case 1:
return "line_strip";
case 2:
return "line_loop";
default:
return "lines";
}
}
inline std::pair<std::string, std::string> readTrackTailEntry(std::istream& in, const StringTable& table) {
const std::uint8_t code = io::readU8(in);
std::string key;
if (code == detail::kTrackTailCustom) {
key = table.resolve(io::readU32(in));
} else {
key = trackTailKeywordName(code);
}
return {std::move(key), table.resolve(io::readU32(in))};
}
// --- TERR: jednorodny teren trójkątowy (materiał/lighting w nagłówku chunka) ---
inline constexpr std::uint8_t kTerrFlagTranslucent = 1u << 0;
inline constexpr std::uint8_t kTerrFlagNonDefaultLighting = 1u << 1;
inline constexpr std::uint8_t kTerrFlagBatched = 1u << 2;
inline constexpr std::size_t kTerrVertsPerRecord = 3;
// basic_region::section() w maszyna-fresh/scene/scene.cpp
inline constexpr std::int32_t kEu07SectionSize = 1000;
inline constexpr std::int32_t kEu07RegionSideSectionCount = 500;
struct TerrSectionKey {
std::int32_t x = 0;
std::int32_t z = 0;
[[nodiscard]] bool operator==(const TerrSectionKey& other) const noexcept {
return x == other.x && z == other.z;
}
};
struct TerrSectionKeyHash {
[[nodiscard]] std::size_t operator()(const TerrSectionKey& key) const noexcept {
return static_cast<std::size_t>(key.x) * 0x9E3779B9u ^
static_cast<std::size_t>(key.z);
}
};
struct TerrSectionBatch {
TerrSectionKey section{};
std::vector<runtime::WorldVertex> vertices;
};
[[nodiscard]] inline runtime::Vec3 terrTriangleCentroid(
const std::array<runtime::WorldVertex, kTerrVertsPerRecord>& triangle) noexcept {
runtime::Vec3 center{};
for (const runtime::WorldVertex& vertex : triangle) {
center.x += vertex.position.x;
center.y += vertex.position.y;
center.z += vertex.position.z;
}
const double inv = 1.0 / static_cast<double>(kTerrVertsPerRecord);
center.x *= inv;
center.y *= inv;
center.z *= inv;
return center;
}
[[nodiscard]] inline TerrSectionKey terrSectionKey(const runtime::Vec3& centroid) noexcept {
TerrSectionKey key;
key.x = static_cast<std::int32_t>(std::floor(
centroid.x / static_cast<double>(kEu07SectionSize) +
static_cast<double>(kEu07RegionSideSectionCount) / 2.0));
key.z = static_cast<std::int32_t>(std::floor(
centroid.z / static_cast<double>(kEu07SectionSize) +
static_cast<double>(kEu07RegionSideSectionCount) / 2.0));
return key;
}
[[nodiscard]] inline bool nearZeroVec3(const runtime::Vec3& v, const double eps = 1e-9) noexcept {
return std::abs(v.x) <= eps && std::abs(v.y) <= eps && std::abs(v.z) <= eps;
}
[[nodiscard]] inline bool lightingEqual(
const runtime::LightingData& a,
const runtime::LightingData& b) noexcept {
const auto near = [&](const float x, const float y) noexcept {
return std::abs(x - y) <= detail::kLightingEps;
};
return near(a.diffuse.x, b.diffuse.x) && near(a.diffuse.y, b.diffuse.y) &&
near(a.diffuse.z, b.diffuse.z) && near(a.diffuse.w, b.diffuse.w) &&
near(a.ambient.x, b.ambient.x) && near(a.ambient.y, b.ambient.y) &&
near(a.ambient.z, b.ambient.z) && near(a.ambient.w, b.ambient.w) &&
near(a.specular.x, b.specular.x) && near(a.specular.y, b.specular.y) &&
near(a.specular.z, b.specular.z) && near(a.specular.w, b.specular.w);
}
[[nodiscard]] inline bool terrMetadataMatches(
const runtime::RuntimeShapeNode& a,
const runtime::RuntimeShapeNode& b) noexcept {
return a.materialPath == b.materialPath && a.translucent == b.translucent &&
lightingEqual(a.lighting, b.lighting);
}
[[nodiscard]] inline bool isTerrTriangleLite(const runtime::RuntimeShapeNode& shape) noexcept {
const std::string_view nodeType =
shape.node.nodeType.empty() ? std::string_view{"triangles"} : std::string_view{shape.node.nodeType};
if (meshSubtypeCode(nodeType) != 0) {
return false;
}
if (shape.vertices.size() != kTerrVertsPerRecord) {
return false;
}
if (!nearZeroVec3(shape.origin)) {
return false;
}
if (!shape.node.name.empty()) {
return false;
}
if (shape.node.rangeSquaredMin != 0.0) {
return false;
}
if (detail::hasNonDefaultRangeMax(shape.node.rangeSquaredMax)) {
return false;
}
if (!shape.node.visible) {
return false;
}
if (shape.node.groupValid) {
return false;
}
if (detail::hasNonEmptyTransform(shape.node.transform)) {
return false;
}
return true;
}
[[nodiscard]] inline bool canUseTerrEncoding(
const std::vector<runtime::RuntimeShapeNode>& shapes) noexcept {
if (shapes.empty()) {
return false;
}
if (!isTerrTriangleLite(shapes.front())) {
return false;
}
for (std::size_t i = 1; i < shapes.size(); ++i) {
if (!isTerrTriangleLite(shapes[i])) {
return false;
}
if (!terrMetadataMatches(shapes.front(), shapes[i])) {
return false;
}
}
return true;
}
[[nodiscard]] inline std::uint8_t terrFlagsForShape(
const runtime::RuntimeShapeNode& shape,
const bool batched = false) noexcept {
std::uint8_t flags = 0;
if (shape.translucent) {
flags |= kTerrFlagTranslucent;
}
if (!isDefaultLighting(shape.lighting)) {
flags |= kTerrFlagNonDefaultLighting;
}
if (batched) {
flags |= kTerrFlagBatched;
}
return flags;
}
[[nodiscard]] inline std::vector<TerrSectionBatch> groupTerrShapesBySection(
const std::vector<runtime::RuntimeShapeNode>& shapes) {
std::unordered_map<TerrSectionKey, std::size_t, TerrSectionKeyHash> batchIndex;
std::vector<TerrSectionBatch> batches;
batches.reserve(256);
for (const runtime::RuntimeShapeNode& shape : shapes) {
if (shape.vertices.size() != kTerrVertsPerRecord) {
throw std::runtime_error("EU7B TERR: oczekiwano 3 wierzcholkow na trojkat");
}
std::array<runtime::WorldVertex, kTerrVertsPerRecord> triangle{};
for (std::size_t i = 0; i < kTerrVertsPerRecord; ++i) {
triangle[i] = shape.vertices[i];
}
const TerrSectionKey key = terrSectionKey(terrTriangleCentroid(triangle));
const auto [it, inserted] = batchIndex.emplace(key, batches.size());
if (inserted) {
TerrSectionBatch batch;
batch.section = key;
batches.push_back(std::move(batch));
}
TerrSectionBatch& batch = batches[it->second];
batch.vertices.insert(
batch.vertices.end(),
shape.vertices.begin(),
shape.vertices.end());
}
std::sort(
batches.begin(),
batches.end(),
[](const TerrSectionBatch& a, const TerrSectionBatch& b) noexcept {
if (a.section.z != b.section.z) {
return a.section.z < b.section.z;
}
return a.section.x < b.section.x;
});
return batches;
}
struct ModelSectionBatch {
TerrSectionKey section{};
std::vector<runtime::RuntimeModelInstance> models;
};
[[nodiscard]] inline TerrSectionKey clampTerrSectionKey(const TerrSectionKey key) noexcept {
TerrSectionKey clamped = key;
clamped.x = std::clamp(
clamped.x,
0,
static_cast<std::int32_t>(kEu07RegionSideSectionCount) - 1);
clamped.z = std::clamp(
clamped.z,
0,
static_cast<std::int32_t>(kEu07RegionSideSectionCount) - 1);
return clamped;
}
[[nodiscard]] inline std::vector<ModelSectionBatch> groupModelsBySection(
const std::vector<runtime::RuntimeModelInstance>& models) {
std::unordered_map<TerrSectionKey, std::size_t, TerrSectionKeyHash> batchIndex;
std::vector<ModelSectionBatch> batches;
batches.reserve(256);
for (const runtime::RuntimeModelInstance& model : models) {
const TerrSectionKey key = clampTerrSectionKey(terrSectionKey(model.location));
const auto [it, inserted] = batchIndex.emplace(key, batches.size());
if (inserted) {
ModelSectionBatch batch;
batch.section = key;
batches.push_back(std::move(batch));
}
batches[it->second].models.push_back(model);
}
std::sort(
batches.begin(),
batches.end(),
[](const ModelSectionBatch& a, const ModelSectionBatch& b) noexcept {
if (a.section.z != b.section.z) {
return a.section.z < b.section.z;
}
return a.section.x < b.section.x;
});
return batches;
}
} // namespace eu07::scene::binary::codec

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
#pragma once
// Self-test: RuntimeModule → tokeny → processScene → bake → porównanie liczników.
#include <eu07/scene/bake/module.hpp>
#include <eu07/scene/binary/detokenize.hpp>
#include <eu07/scene/processor.hpp>
#include <format>
#include <string>
#include <vector>
namespace eu07::scene::binary::selftest {
struct ModuleSnapshot {
std::size_t includes = 0;
std::size_t tracks = 0;
std::size_t traction = 0;
std::size_t powerSources = 0;
std::size_t shapes = 0;
std::size_t lines = 0;
std::size_t models = 0;
std::size_t memcells = 0;
std::size_t eventLaunchers = 0;
std::size_t dynamics = 0;
std::size_t sounds = 0;
std::size_t trainsets = 0;
std::size_t events = 0;
std::uint32_t firstInit = 0;
std::size_t meshVertices = 0;
std::size_t lineVertices = 0;
};
[[nodiscard]] inline ModuleSnapshot snapshotModule(const bake::RuntimeModule& module) {
ModuleSnapshot snap;
snap.includes = module.includes.size();
snap.tracks = module.scene.tracks.size();
snap.traction = module.scene.traction.size();
snap.powerSources = module.scene.powerSources.size();
snap.shapes = module.scene.shapes.size();
snap.lines = module.scene.lines.size();
snap.models = module.scene.models.size();
snap.memcells = module.scene.memcells.size();
snap.eventLaunchers = module.scene.eventLaunchers.size();
snap.dynamics = module.scene.dynamics.size();
snap.sounds = module.scene.sounds.size();
snap.trainsets = module.scene.trainsets.size();
snap.events = module.scene.events.size();
snap.firstInit = module.scene.firstInitCount;
for (const runtime::RuntimeShapeNode& shape : module.scene.shapes) {
snap.meshVertices += shape.vertices.size();
}
for (const runtime::RuntimeLinesNode& line : module.scene.lines) {
snap.lineVertices += line.vertices.size();
}
return snap;
}
[[nodiscard]] inline std::string diffSnapshots(
const ModuleSnapshot& expected,
const ModuleSnapshot& actual) {
std::string report;
const auto field = [&](const char* label, const std::size_t a, const std::size_t b) {
if (a != b) {
report += std::format(" {}: {} -> {}\n", label, a, b);
}
};
field("include", expected.includes, actual.includes);
field("track", expected.tracks, actual.tracks);
field("traction", expected.traction, actual.traction);
field("power", expected.powerSources, actual.powerSources);
field("mesh", expected.shapes, actual.shapes);
field("mesh_vert", expected.meshVertices, actual.meshVertices);
field("line", expected.lines, actual.lines);
field("line_vert", expected.lineVertices, actual.lineVertices);
field("model", expected.models, actual.models);
field("memcell", expected.memcells, actual.memcells);
field("launcher", expected.eventLaunchers, actual.eventLaunchers);
field("dynamic", expected.dynamics, actual.dynamics);
field("sound", expected.sounds, actual.sounds);
field("trainset", expected.trainsets, actual.trainsets);
field("event", expected.events, actual.events);
field("firstInit", expected.firstInit, actual.firstInit);
return report;
}
[[nodiscard]] inline bool snapshotsEqual(
const ModuleSnapshot& expected,
const ModuleSnapshot& actual) {
return expected.includes == actual.includes && expected.tracks == actual.tracks &&
expected.traction == actual.traction && expected.powerSources == actual.powerSources &&
expected.shapes == actual.shapes && expected.meshVertices == actual.meshVertices &&
expected.lines == actual.lines && expected.lineVertices == actual.lineVertices &&
expected.models == actual.models && expected.memcells == actual.memcells &&
expected.eventLaunchers == actual.eventLaunchers &&
expected.dynamics == actual.dynamics && expected.sounds == actual.sounds &&
expected.trainsets == actual.trainsets && expected.events == actual.events &&
expected.firstInit == actual.firstInit;
}
struct RoundTripResult {
bool passed = false;
ModuleSnapshot original;
ModuleSnapshot roundtrip;
std::size_t tokenCount = 0;
std::size_t unknownTokens = 0;
std::string diff;
std::vector<SourceToken> tokens;
};
[[nodiscard]] inline RoundTripResult runRoundTrip(const bake::RuntimeModule& module) {
RoundTripResult result;
result.original = snapshotModule(module);
result.tokens = detokenize::runtimeModuleToTokens(module);
result.tokenCount = result.tokens.size();
eu07::ParseResult parsed;
parsed.tokens = result.tokens;
SceneProcessOptions options;
options.expandIncludes = false;
const SceneProcessResult processed = processScene(parsed, {}, options);
result.unknownTokens = processed.document.unknown.size();
const bake::RuntimeModule rebaked = bake::bakeModule(processed.document);
result.roundtrip = snapshotModule(rebaked);
result.diff = diffSnapshots(result.original, result.roundtrip);
result.passed = snapshotsEqual(result.original, result.roundtrip) && result.unknownTokens == 0;
return result;
}
} // namespace eu07::scene::binary::selftest

View File

@@ -0,0 +1,75 @@
#pragma once
#include <cstdint>
#include <memory>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace eu07::scene::binary {
class StringTable {
public:
static constexpr std::uint32_t kEmpty = 0xFFFFFFFFu;
StringTable() = default;
StringTable(const StringTable&) = delete;
StringTable& operator=(const StringTable&) = delete;
StringTable(StringTable&&) noexcept = default;
StringTable& operator=(StringTable&&) noexcept = default;
[[nodiscard]] std::uint32_t intern(const std::string& text) {
if (text.empty()) {
return kEmpty;
}
{
std::shared_lock read_lock(*mutex_);
if (const auto found = index_.find(text); found != index_.end()) {
return found->second;
}
}
std::unique_lock write_lock(*mutex_);
if (const auto found = index_.find(text); found != index_.end()) {
return found->second;
}
const std::uint32_t id = static_cast<std::uint32_t>(strings_.size());
strings_.push_back(text);
index_.emplace(strings_.back(), id);
return id;
}
[[nodiscard]] const std::string& resolve(const std::uint32_t id) const {
static const std::string empty;
if (id == kEmpty || id >= strings_.size()) {
return empty;
}
return strings_[id];
}
[[nodiscard]] const std::vector<std::string>& strings() const noexcept {
return strings_;
}
void load(std::vector<std::string> strings) {
strings_ = std::move(strings);
index_.clear();
index_.reserve(strings_.size());
for (std::uint32_t i = 0; i < strings_.size(); ++i) {
index_.emplace(strings_[i], i);
}
}
void mergeFrom(const StringTable& other) {
for (const std::string& text : other.strings()) {
intern(text);
}
}
private:
std::unique_ptr<std::shared_mutex> mutex_ { std::make_unique<std::shared_mutex>() };
std::vector<std::string> strings_;
std::unordered_map<std::string, std::uint32_t> index_;
};
} // namespace eu07::scene::binary

View File

@@ -0,0 +1,112 @@
#pragma once
#include <eu07/parser.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <string>
#include <vector>
namespace eu07::scene::endgroup {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "endgroup")) {
return false;
}
const SourceToken endToken = stream.consume();
DirectiveBlock block;
block.line = endToken.sourceLine;
block.tokens.push_back(endToken);
ctx.scratch.closeGroup();
ctx.document.group.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::endgroup
namespace eu07::scene::endorigin {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "endorigin")) {
return false;
}
const SourceToken endToken = stream.consume();
DirectiveBlock block;
block.line = endToken.sourceLine;
block.tokens.push_back(endToken);
ctx.scratch.popOrigin();
ctx.document.origin.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::endorigin
namespace eu07::scene::endscale {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "endscale")) {
return false;
}
const SourceToken endToken = stream.consume();
DirectiveBlock block;
block.line = endToken.sourceLine;
block.tokens.push_back(endToken);
ctx.scratch.popScale();
ctx.document.scale.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::endscale
namespace eu07::scene::assignment {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "assignment")) {
return false;
}
stream.consume();
std::vector<SourceToken> raw;
while (!stream.empty() && !isKeyword(stream.peek().value, "endassignment")) {
std::string key;
std::string value;
if (!node::io::takeString(stream, raw, key) || !node::io::takeString(stream, raw, value)) {
return false;
}
if (ctx.scratch.trainset.isOpen) {
ctx.scratch.trainset.assignment.emplace_back(std::move(key), std::move(value));
}
}
if (!stream.empty() && isKeyword(stream.peek().value, "endassignment")) {
stream.consume();
}
return true;
}
} // namespace eu07::scene::assignment
namespace eu07::scene::endtrainset {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "endtrainset")) {
return false;
}
const SourceToken endToken = stream.consume();
if (ctx.scratch.trainset.isOpen && ctx.scratch.trainset.docIndex) {
ctx.document.trainset[*ctx.scratch.trainset.docIndex].tokens.push_back(endToken);
}
ctx.scratch.closeTrainset();
return true;
}
} // namespace eu07::scene::endtrainset

View File

@@ -0,0 +1,35 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_camera
// camera position rotation index endcamera
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::camera {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "camera")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endcamera")) {
session.take();
break;
}
session.take();
}
ctx.document.camera.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::camera

View File

@@ -0,0 +1,35 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_config
// config settings endconfig
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::config {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "config")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endconfig")) {
session.take();
break;
}
session.take();
}
ctx.document.config.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::config

View File

@@ -0,0 +1,35 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/scratch.hpp>
#include <filesystem>
#include <vector>
namespace eu07::scene {
struct ParseContext {
SceneDocument& document;
SceneScratchpad scratch;
std::filesystem::path baseDirectory;
std::vector<std::filesystem::path> includeStack;
std::vector<std::vector<std::string>> activeIncludeParameters;
bool expandIncludes = true;
};
using DirectiveParser = bool (*)(TokenStream& stream, ParseContext& context);
namespace detail {
struct IncludeExpansionRequest {
std::size_t entryIndex = 0;
std::string file;
std::vector<std::string> parameters;
};
void expandInclude(ParseContext& context, const IncludeExpansionRequest& request);
} // namespace detail
} // namespace eu07::scene

View File

@@ -0,0 +1,58 @@
#pragma once
#include <eu07/parser.hpp>
#include <cstddef>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace eu07::scene {
class TokenStream {
public:
explicit TokenStream(const std::vector<SourceToken>& tokens) noexcept
: tokens_(&tokens) {}
[[nodiscard]] bool empty() const noexcept { return index_ >= tokens_->size(); }
[[nodiscard]] const SourceToken& peek() const {
if (empty()) {
throw std::runtime_error("TokenStream: koniec strumienia");
}
return (*tokens_)[index_];
}
[[nodiscard]] SourceToken consume() {
SourceToken token = peek();
++index_;
return token;
}
void skip(const std::size_t count = 1) {
if (index_ + count > tokens_->size()) {
throw std::runtime_error("TokenStream: przekroczenie konca");
}
index_ += count;
}
[[nodiscard]] std::size_t index() const noexcept { return index_; }
[[nodiscard]] std::size_t remaining() const noexcept {
return tokens_->size() - index_;
}
[[nodiscard]] bool peekIs(const std::string_view keyword) const {
return !empty() && peek().value == keyword;
}
[[nodiscard]] std::size_t checkpoint() const noexcept { return index_; }
void rewind(const std::size_t position) { index_ = position; }
private:
const std::vector<SourceToken>* tokens_;
std::size_t index_ = 0;
};
} // namespace eu07::scene

View File

@@ -0,0 +1,35 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_description
// description text enddescription
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::description {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "description")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("enddescription")) {
session.take();
break;
}
session.take();
}
ctx.document.description.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::description

View File

@@ -0,0 +1,25 @@
#pragma once
#include <eu07/scene/match.hpp>
#include <string_view>
namespace eu07::scene::detail {
// Token w srodku wpisu node (np. isolated NazwaToru, velocity, endtrack).
[[nodiscard]] inline bool isEmbeddedInNode(const std::string_view token) noexcept {
return isKeyword(token, "velocity") || isKeyword(token, "rail_screw_used1") ||
isKeyword(token, "endtrack") || isKeyword(token, "endtraction") ||
isKeyword(token, "endtri") || isKeyword(token, "endtriangles") ||
isKeyword(token, "enddynamic") || isKeyword(token, "endmodel") ||
isKeyword(token, "vis") || isKeyword(token, "flat");
}
[[nodiscard]] inline bool isTopLevelStarter(const std::string_view token) noexcept {
return isKeyword(token, "node") || isKeyword(token, "include") ||
isKeyword(token, "trainset") || isKeyword(token, "event") ||
isKeyword(token, "rotate") || isKeyword(token, "origin") ||
isKeyword(token, "scale") || isKeyword(token, "group");
}
} // namespace eu07::scene::detail

View File

@@ -0,0 +1,70 @@
#pragma once
// Pomocniki: po trafieniu dyrektywy kazdy token jawnie (peek -> decyzja -> consume).
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::detail {
class ParseSession {
public:
ParseSession(TokenStream& stream, DirectiveBlock& block, const std::size_t anchor) noexcept
: stream_(stream)
, block_(block)
, anchor_(anchor) {}
[[nodiscard]] bool empty() const noexcept { return stream_.empty(); }
[[nodiscard]] const SourceToken& peek() const { return stream_.peek(); }
[[nodiscard]] std::size_t line() const noexcept { return block_.line; }
[[nodiscard]] bool at(const std::string_view keyword) const noexcept {
return !empty() && isKeyword(peek().value, keyword);
}
[[nodiscard]] bool atIgnoreCase(const std::string_view keyword) const noexcept {
return !empty() && isKeywordIgnoreCase(peek().value, keyword);
}
[[nodiscard]] bool onSameLine() const noexcept {
return !empty() && peek().sourceLine == block_.line;
}
SourceToken take() {
SourceToken token = stream_.consume();
block_.tokens.push_back(token);
return token;
}
[[nodiscard]] bool takeIf(const std::string_view keyword) {
if (!at(keyword)) {
return false;
}
take();
return true;
}
[[nodiscard]] bool takeIfIgnoreCase(const std::string_view keyword) {
if (!atIgnoreCase(keyword)) {
return false;
}
take();
return true;
}
void fail() noexcept {
stream_.rewind(anchor_);
block_.tokens.clear();
block_.line = 0;
}
private:
TokenStream& stream_;
DirectiveBlock& block_;
std::size_t anchor_;
};
} // namespace eu07::scene::detail

View File

@@ -0,0 +1,35 @@
#pragma once
#include <eu07/scene/match.hpp>
#include <array>
#include <string_view>
namespace eu07::scene::detail {
[[nodiscard]] inline bool isNodeSubtype(const std::string_view token) noexcept {
static constexpr std::array<std::string_view, 14> kSubtypes{{
"dynamic",
"model",
"track",
"traction",
"tractionpowersource",
"triangles",
"triangle_strip",
"triangle_fan",
"lines",
"line_strip",
"line_loop",
"memcell",
"eventlauncher",
"sound",
}};
for (const std::string_view subtype : kSubtypes) {
if (isKeyword(token, subtype)) {
return true;
}
}
return false;
}
} // namespace eu07::scene::detail

View File

@@ -0,0 +1,110 @@
#pragma once
// Jedna tabela: płaski dispatch jak w simulationstateserializer.cpp (bez processNested).
#include <eu07/scene/area.hpp>
#include <eu07/scene/atmo.hpp>
#include <eu07/scene/boundaries.hpp>
#include <eu07/scene/camera.hpp>
#include <eu07/scene/config.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/description.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/event.hpp>
#include <eu07/scene/first_init.hpp>
#include <eu07/scene/group.hpp>
#include <eu07/scene/include.hpp>
#include <eu07/scene/isolated.hpp>
#include <eu07/scene/light.hpp>
#include <eu07/scene/lua.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node.hpp>
#include <eu07/scene/origin.hpp>
#include <eu07/scene/rotate.hpp>
#include <eu07/scene/scale.hpp>
#include <eu07/scene/sky.hpp>
#include <eu07/scene/terrain.hpp>
#include <eu07/scene/test.hpp>
#include <eu07/scene/time.hpp>
#include <eu07/scene/trainset.hpp>
#include <array>
#include <string_view>
namespace eu07::scene {
struct DirectiveEntry {
std::string_view keyword;
DirectiveParser parse;
};
namespace detail {
[[nodiscard]] inline bool matchOrigin(const std::string_view token) noexcept {
return isKeyword(token, "origin") || isKeyword(token, "orgin");
}
[[nodiscard]] inline bool matchFirstInit(const std::string_view token) noexcept {
return isKeywordIgnoreCase(token, "FirstInit");
}
[[nodiscard]] inline bool matchesEntry(const DirectiveEntry& entry, const std::string_view token) noexcept {
if (entry.keyword == "origin") {
return matchOrigin(token);
}
if (entry.keyword == "FirstInit") {
return matchFirstInit(token);
}
return isKeyword(token, entry.keyword);
}
inline constexpr std::array<DirectiveEntry, 27> kDirectiveTable{{
{"trainset", trainset::parse},
{"endtrainset", endtrainset::parse},
{"assignment", assignment::parse},
{"include", scn_include::parse},
{"FirstInit", first_init::parse},
{"origin", origin::parse},
{"endorigin", endorigin::parse},
{"scale", scale::parse},
{"endscale", endscale::parse},
{"rotate", rotate::parse},
{"group", group::parse},
{"endgroup", endgroup::parse},
{"test", test::parse},
{"node", node::parse},
{"event", event::parse},
{"atmo", atmo::parse},
{"sky", sky::parse},
{"time", time::parse},
{"camera", camera::parse},
{"config", config::parse},
{"lua", lua::parse},
{"isolated", isolated::parse},
{"area", area::parse},
{"terrain", terrain::parse},
{"description", description::parse},
{"light", light::parse},
}};
[[nodiscard]] inline bool dispatchDirective(TokenStream& stream, ParseContext& context) {
if (stream.empty()) {
return false;
}
const std::string_view head = stream.peek().value;
for (const DirectiveEntry& entry : kDirectiveTable) {
if (!matchesEntry(entry, head)) {
continue;
}
if (entry.parse(stream, context)) {
return true;
}
}
return false;
}
} // namespace detail
} // namespace eu07::scene

View File

@@ -0,0 +1,109 @@
#pragma once
#include <eu07/parser.hpp>
#include <eu07/scene/include_types.hpp>
#include <eu07/scene/node/dynamic.hpp>
#include <eu07/scene/node/eventlauncher.hpp>
#include <eu07/scene/node/line_loop.hpp>
#include <eu07/scene/node/line_strip.hpp>
#include <eu07/scene/node/lines.hpp>
#include <eu07/scene/node/memcell.hpp>
#include <eu07/scene/node/model.hpp>
#include <eu07/scene/node/sound.hpp>
#include <eu07/scene/node/track.hpp>
#include <eu07/scene/node/traction.hpp>
#include <eu07/scene/node/traction_power.hpp>
#include <eu07/scene/node/triangle_fan.hpp>
#include <eu07/scene/node/triangle_strip.hpp>
#include <eu07/scene/node/triangles.hpp>
#include <cstddef>
#include <string>
#include <vector>
namespace eu07::scene {
struct DirectiveBlock {
std::size_t line = 0;
std::vector<SourceToken> tokens;
};
struct UnknownEntry {
std::size_t line = 0;
std::string token;
};
struct SceneDocument {
std::vector<DirectiveBlock> atmo;
std::vector<DirectiveBlock> sky;
std::vector<DirectiveBlock> time;
std::vector<DirectiveBlock> firstInit;
std::vector<DirectiveBlock> trainset;
std::vector<DirectiveBlock> event;
std::vector<ParsedInclude> include;
std::vector<DirectiveBlock> camera;
std::vector<DirectiveBlock> config;
std::vector<DirectiveBlock> lua;
std::vector<ParsedNodeDynamic> nodeDynamic;
std::vector<ParsedNodeModel> nodeModel;
std::vector<ParsedTrackNormal> nodeTrackNormal;
std::vector<ParsedTrackSwitch> nodeTrackSwitch;
std::vector<ParsedTrackRoad> nodeTrackRoad;
std::vector<ParsedTrackCross> nodeTrackCross;
std::vector<ParsedTrackOther> nodeTrackOther;
std::vector<ParsedNodeTraction> nodeTraction;
std::vector<ParsedNodeTractionPowerSource> nodeTractionPower;
std::vector<ParsedNodeTriangles> nodeTriangles;
std::vector<ParsedNodeTriangleStrip> nodeTriangleStrip;
std::vector<ParsedNodeTriangleFan> nodeTriangleFan;
std::vector<ParsedNodeLines> nodeLines;
std::vector<ParsedNodeLineStrip> nodeLineStrip;
std::vector<ParsedNodeLineLoop> nodeLineLoop;
std::vector<ParsedNodeMemcell> nodeMemcell;
std::vector<ParsedNodeEventlauncher> nodeEventlauncher;
std::vector<ParsedNodeSound> nodeSound;
std::vector<DirectiveBlock> origin;
std::vector<DirectiveBlock> scale;
std::vector<DirectiveBlock> rotate;
std::vector<DirectiveBlock> group;
std::vector<DirectiveBlock> isolated;
std::vector<DirectiveBlock> area;
std::vector<DirectiveBlock> terrain;
std::vector<DirectiveBlock> description;
std::vector<DirectiveBlock> light;
std::vector<DirectiveBlock> test;
std::vector<UnknownEntry> unknown;
};
struct SceneProcessResult {
SceneDocument document;
};
[[nodiscard]] inline std::size_t countTrackInstances(const SceneDocument& doc) noexcept {
return doc.nodeTrackNormal.size() + doc.nodeTrackSwitch.size() + doc.nodeTrackRoad.size() +
doc.nodeTrackCross.size() + doc.nodeTrackOther.size();
}
[[nodiscard]] inline std::size_t countNodeInstances(const SceneDocument& doc) noexcept {
return doc.nodeDynamic.size() + doc.nodeModel.size() + countTrackInstances(doc) +
doc.nodeTraction.size() + doc.nodeTractionPower.size() + doc.nodeTriangles.size() +
doc.nodeTriangleStrip.size() +
doc.nodeTriangleFan.size() + doc.nodeLines.size() + doc.nodeLineStrip.size() +
doc.nodeLineLoop.size() + doc.nodeMemcell.size() + doc.nodeEventlauncher.size() +
doc.nodeSound.size();
}
[[nodiscard]] inline std::size_t countDirectiveInstances(const SceneDocument& doc) noexcept {
return doc.atmo.size() + doc.sky.size() + doc.time.size() + doc.firstInit.size() +
doc.trainset.size() + doc.event.size() + doc.include.size() + doc.camera.size() +
doc.config.size() + doc.lua.size() + countNodeInstances(doc) + doc.origin.size() +
doc.scale.size() + doc.rotate.size() + doc.group.size() + doc.isolated.size() +
doc.area.size() + doc.terrain.size() + doc.description.size() + doc.light.size() +
doc.test.size();
}
} // namespace eu07::scene

View File

@@ -0,0 +1,37 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_event
// event name type delay … endevent
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::event {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "event")) {
return false;
}
DirectiveBlock block;
SourceToken head = stream.consume();
block.line = head.sourceLine;
block.tokens.push_back(std::move(head));
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endevent")) {
session.take();
break;
}
session.take();
}
ctx.document.event.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::event

View File

@@ -0,0 +1,23 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_FirstInit
// FirstInit — samo slowo kluczowe, bez end*
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::first_init {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeywordIgnoreCase(stream.peek().value, "FirstInit")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
ctx.document.firstInit.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::first_init

View File

@@ -0,0 +1,24 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_group
// group … endgroup — jak oryginal: tylko push/pop grupy, bez połykania tokenów.
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::group {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "group")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
ctx.scratch.openGroup();
ctx.document.group.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::group

View File

@@ -0,0 +1,73 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_include
// include file parameters end — ekspansja rekurencyjna (wariant 2)
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/include_resolve.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/parse_util.hpp>
namespace eu07::scene::scn_include {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "include")) {
return false;
}
ParsedInclude entry;
SourceToken head = stream.consume();
entry.line = head.sourceLine;
entry.raw.push_back(head);
if (stream.empty()) {
entry.error = "brak sciezki pliku";
ctx.document.include.push_back(std::move(entry));
return true;
}
SourceToken fileToken;
if (!node::io::takeToken(stream, entry.raw, fileToken)) {
entry.error = "brak sciezki pliku";
ctx.document.include.push_back(std::move(entry));
return true;
}
entry.file = fileToken.value;
while (!stream.empty()) {
if (isKeyword(stream.peek().value, "end")) {
entry.raw.push_back(stream.consume());
break;
}
SourceToken param;
if (!node::io::takeToken(stream, entry.raw, param)) {
break;
}
std::string value = param.value;
if (!ctx.activeIncludeParameters.empty()) {
value = detail::applyIncludeParameters(value, ctx.activeIncludeParameters.back());
}
entry.parameters.push_back(std::move(value));
}
entry.siteTransform.originStack = ctx.scratch.location.originStack;
entry.siteTransform.scaleStack = ctx.scratch.location.scaleStack;
entry.siteTransform.rotation = ctx.scratch.location.rotation;
entry.siteTransform.groupStackDepth = ctx.scratch.group.activeStack.size();
const std::size_t includeIndex = ctx.document.include.size();
ctx.document.include.push_back(std::move(entry));
if (ctx.expandIncludes) {
detail::IncludeExpansionRequest request;
request.entryIndex = includeIndex;
request.file = ctx.document.include[includeIndex].file;
request.parameters = ctx.document.include[includeIndex].parameters;
detail::expandInclude(ctx, request);
}
return true;
}
} // namespace eu07::scene::scn_include

View File

@@ -0,0 +1,66 @@
#pragma once
// Mapowanie origin/rotate z .inc na indeksy parametrow INCL — bez heurystyki w runtime.
#include <eu07/scene/document.hpp>
#include <eu07/scene/include_resolve.hpp>
#include <cstdint>
#include <optional>
#include <string_view>
namespace eu07::scene {
struct IncludePlacement {
std::uint8_t origin_x_param = 0;
std::uint8_t origin_y_param = 0;
std::uint8_t origin_z_param = 0;
std::uint8_t rotation_y_param = 0;
[[nodiscard]] bool empty() const noexcept {
return origin_x_param == 0 && origin_y_param == 0 && origin_z_param == 0 &&
rotation_y_param == 0;
}
};
namespace detail {
[[nodiscard]] inline std::uint8_t placementParamFromToken(const std::string_view text) {
std::uint8_t index = 0;
if (parseIncludeParameterIndex(text, index)) {
return index;
}
return 0;
}
} // namespace detail
[[nodiscard]] inline std::optional<IncludePlacement> extractIncludePlacement(const SceneDocument& document) {
if (document.origin.empty()) {
return std::nullopt;
}
const DirectiveBlock& originBlock = document.origin.front();
if (originBlock.tokens.size() < 3) {
return std::nullopt;
}
IncludePlacement placement;
placement.origin_x_param = detail::placementParamFromToken(originBlock.tokens[0].value);
placement.origin_y_param = detail::placementParamFromToken(originBlock.tokens[1].value);
placement.origin_z_param = detail::placementParamFromToken(originBlock.tokens[2].value);
if (!document.rotate.empty()) {
const DirectiveBlock& rotateBlock = document.rotate.front();
if (rotateBlock.tokens.size() >= 2) {
placement.rotation_y_param = detail::placementParamFromToken(rotateBlock.tokens[1].value);
}
}
if (placement.empty()) {
return std::nullopt;
}
return placement;
}
} // namespace eu07::scene

View File

@@ -0,0 +1,619 @@
#pragma once
#include <eu07/parser.hpp>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace eu07::scene::detail {
enum class IncludeScanMode { Code, LineComment, BlockComment, QuotedString };
[[nodiscard]] inline std::string normalizeIncludePathToken(std::string path) {
std::replace(path.begin(), path.end(), '\\', '/');
return path;
}
[[nodiscard]] inline std::filesystem::path resolveIncludePath(
const std::filesystem::path& baseDirectory,
const std::string& fileToken) {
const std::filesystem::path relative(normalizeIncludePathToken(fileToken));
if (relative.is_absolute()) {
return relative;
}
return baseDirectory / relative;
}
[[nodiscard]] inline bool isIncFile(const std::string& fileToken) {
if (fileToken.size() < 4) {
return false;
}
const std::string_view tail(fileToken.data() + fileToken.size() - 4, 4);
std::string lower(tail);
for (char& ch : lower) {
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
}
return lower == ".inc";
}
[[nodiscard]] inline bool tryParseIncludeParameterRef(
const std::string_view text,
const std::size_t pos,
std::size_t& endOut) {
if (pos + 3 >= text.size() || text[pos] != '(' || text[pos + 1] != 'p') {
return false;
}
std::size_t i = pos + 2;
if (i >= text.size() || !std::isdigit(static_cast<unsigned char>(text[i]))) {
return false;
}
while (i < text.size() && std::isdigit(static_cast<unsigned char>(text[i]))) {
++i;
}
if (i >= text.size() || text[i] != ')') {
return false;
}
endOut = i;
return true;
}
[[nodiscard]] inline bool parseIncludeParameterIndex(
const std::string_view text,
std::uint8_t& indexOut) {
std::size_t end = 0;
if (!tryParseIncludeParameterRef(text, 0, end) || end + 1 != text.size()) {
return false;
}
std::size_t value = 0;
for (std::size_t i = 2; i < text.size() && std::isdigit(static_cast<unsigned char>(text[i])); ++i) {
value = value * 10 + static_cast<std::size_t>(text[i] - '0');
}
if (value == 0 || value > 255) {
return false;
}
indexOut = static_cast<std::uint8_t>(value);
return true;
}
[[nodiscard]] inline std::string resolveIncludeParameter(
const std::string_view indexText,
const std::span<const std::string> parameters) {
std::size_t index = 0;
try {
index = std::stoul(std::string(indexText));
} catch (...) {
index = 0;
}
if (index >= 1 && index <= parameters.size()) {
return parameters[index - 1];
}
return "none";
}
[[nodiscard]] inline std::string applyIncludeParameters(
std::string text,
const std::span<const std::string> parameters) {
if (parameters.empty()) {
return text;
}
std::string out;
out.reserve(text.size());
IncludeScanMode mode = IncludeScanMode::Code;
for (std::size_t i = 0; i < text.size(); ) {
const char ch = text[i];
if (mode == IncludeScanMode::QuotedString) {
out.push_back(ch);
if (ch == '\\' && i + 1 < text.size()) {
out.push_back(text[i + 1]);
i += 2;
continue;
}
if (ch == '"') {
mode = IncludeScanMode::Code;
}
++i;
continue;
}
if (mode == IncludeScanMode::Code) {
if (ch == '"') {
out.push_back(ch);
mode = IncludeScanMode::QuotedString;
++i;
continue;
}
if (ch == '/' && i + 1 < text.size()) {
if (text[i + 1] == '/') {
out.push_back(ch);
out.push_back(text[i + 1]);
i += 2;
mode = IncludeScanMode::LineComment;
continue;
}
if (text[i + 1] == '*') {
out.push_back(ch);
out.push_back(text[i + 1]);
i += 2;
mode = IncludeScanMode::BlockComment;
continue;
}
}
} else if (mode == IncludeScanMode::BlockComment) {
if (ch == '*' && i + 1 < text.size() && text[i + 1] == '/') {
out.push_back(ch);
out.push_back(text[i + 1]);
i += 2;
mode = IncludeScanMode::Code;
continue;
}
}
std::size_t refEnd = 0;
if (tryParseIncludeParameterRef(text, i, refEnd)) {
const std::string replacement = resolveIncludeParameter(
text.substr(i + 2, refEnd - (i + 2)),
parameters);
out.append(replacement);
i = refEnd + 1;
continue;
}
out.push_back(ch);
if (ch == '\n' && mode == IncludeScanMode::LineComment) {
mode = IncludeScanMode::Code;
}
++i;
}
return out;
}
[[nodiscard]] inline eu07::ParseResult parseIncludedFile(
const std::filesystem::path& path,
const std::vector<std::string>& parameters) {
const eu07::RawFile raw = eu07::readRawFile(path);
const std::string expanded = applyIncludeParameters(raw.data, parameters);
return eu07::parseText(expanded);
}
[[nodiscard]] inline bool isIncludeCycle(
const std::filesystem::path& resolved,
const std::vector<std::filesystem::path>& includeStack) {
const std::filesystem::path normalized = resolved.lexically_normal();
for (const std::filesystem::path& entry : includeStack) {
if (entry.lexically_normal() == normalized) {
return true;
}
}
return false;
}
// Jak cParser / eu7_loader::resolve_parser_include_path — wzgledem scenery/, nie parent pliku.
[[nodiscard]] inline std::optional<std::filesystem::path> findSceneryRootInPath(
std::filesystem::path inputPath) {
inputPath = inputPath.lexically_normal();
while (!inputPath.empty()) {
if (inputPath.filename() == "scenery") {
return inputPath;
}
const std::filesystem::path parent = inputPath.parent_path();
if (parent == inputPath) {
break;
}
inputPath = parent;
}
return std::nullopt;
}
[[nodiscard]] inline std::string relativeSceneryFile(
const std::filesystem::path& sceneryRoot,
const std::filesystem::path& absolutePath) {
std::error_code ec;
const std::filesystem::path relative =
std::filesystem::relative(absolutePath.lexically_normal(), sceneryRoot.lexically_normal(), ec);
if (ec) {
return absolutePath.filename().generic_string();
}
return relative.generic_string();
}
[[nodiscard]] inline std::filesystem::path resolveParserIncludePath(
const std::filesystem::path& sceneryRoot,
const std::string& currentRelativeFile,
const std::string& fileToken) {
std::string reference = normalizeIncludePathToken(fileToken);
while (!reference.empty() && reference[0] == '$') {
reference.erase(0, 1);
}
const std::filesystem::path root = sceneryRoot.lexically_normal();
if (reference.find('/') != std::string::npos) {
return root / std::filesystem::path(reference);
}
if (!currentRelativeFile.empty()) {
const std::size_t slash = currentRelativeFile.find_last_of('/');
if (slash != std::string::npos) {
return root / std::filesystem::path(currentRelativeFile.substr(0, slash + 1) + reference);
}
}
return root / std::filesystem::path(reference);
}
[[nodiscard]] inline std::filesystem::path resolveIncludeSourcePath(
const std::filesystem::path& sceneryRoot,
const std::string& currentRelativeFile,
const std::string& fileToken) {
const std::filesystem::path primary =
resolveParserIncludePath(sceneryRoot, currentRelativeFile, fileToken);
if (std::filesystem::exists(primary)) {
return primary;
}
std::filesystem::path stem = primary;
stem.replace_extension();
for (const std::string_view ext : {".scm", ".inc", ".scn"}) {
std::filesystem::path candidate = stem;
candidate.replace_extension(ext);
if (std::filesystem::exists(candidate)) {
return candidate;
}
}
// Include bez katalogu (tree.inc, grass.inc) — plik w korzeniu scenery/, nie w podfolderze parenta.
std::string reference = normalizeIncludePathToken(fileToken);
while (!reference.empty() && reference[0] == '$') {
reference.erase(0, 1);
}
if (reference.find('/') == std::string::npos && primary != sceneryRoot / reference) {
const std::filesystem::path rootCandidate =
sceneryRoot.lexically_normal() / std::filesystem::path(reference);
if (std::filesystem::exists(rootCandidate)) {
return rootCandidate;
}
std::filesystem::path rootStem = rootCandidate;
rootStem.replace_extension();
for (const std::string_view ext : {".scm", ".inc", ".scn"}) {
std::filesystem::path candidate = rootStem;
candidate.replace_extension(ext);
if (std::filesystem::exists(candidate)) {
return candidate;
}
}
}
return primary;
}
[[nodiscard]] inline std::filesystem::path resolveExpandedIncludePath(
const std::filesystem::path& baseDirectory,
const std::vector<std::filesystem::path>& includeStack,
const std::string& fileToken) {
const std::filesystem::path anchor =
baseDirectory.empty() ? std::filesystem::current_path() : baseDirectory;
if (const std::optional<std::filesystem::path> sceneryRoot = findSceneryRootInPath(anchor)) {
std::string currentRelative;
if (!includeStack.empty()) {
currentRelative = relativeSceneryFile(*sceneryRoot, includeStack.back());
}
return resolveIncludeSourcePath(*sceneryRoot, currentRelative, fileToken);
}
return resolveIncludePath(baseDirectory, fileToken);
}
} // namespace eu07::scene::detail

View File

@@ -0,0 +1,47 @@
#pragma once
// Szybkie wyciagniecie sciezek z dyrektyw include — bez processScene / bake.
#include <eu07/parser.hpp>
#include <eu07/scene/match.hpp>
#include <filesystem>
#include <span>
#include <string>
#include <vector>
namespace eu07::scene {
[[nodiscard]] inline std::vector<std::string> scanIncludeFilePaths(
const std::span<const SourceToken> tokens) {
std::vector<std::string> files;
files.reserve(8);
for (std::size_t i = 0; i < tokens.size(); ++i) {
if (!isKeyword(tokens[i].value, "include")) {
continue;
}
if (i + 1 >= tokens.size()) {
break;
}
files.push_back(tokens[i + 1].value);
for (std::size_t j = i + 2; j < tokens.size(); ++j) {
if (isKeyword(tokens[j].value, "end")) {
i = j;
break;
}
}
}
return files;
}
[[nodiscard]] inline std::vector<std::string> scanIncludeFilePaths(
const std::filesystem::path& sourcePath) {
const ParseResult parsed = parseFile(sourcePath);
return scanIncludeFilePaths(parsed.tokens);
}
} // namespace eu07::scene

View File

@@ -0,0 +1,23 @@
#pragma once
#include <eu07/parser.hpp>
#include <eu07/scene/runtime/basic_node.hpp>
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedInclude {
std::size_t line = 0;
std::string file;
std::vector<std::string> parameters;
std::vector<SourceToken> raw;
runtime::TransformContext siteTransform;
bool expanded = false;
std::optional<std::string> error;
};
} // namespace eu07::scene

View File

@@ -0,0 +1,48 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_isolated
// isolated name tracks endisolated
// (nie mylic z isolated Nazwa wewnatrz node::track)
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/boundary.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::isolated {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "isolated")) {
return false;
}
const std::size_t anchor = stream.checkpoint();
DirectiveBlock block;
block.line = stream.consume().sourceLine;
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endisolated")) {
session.take();
ctx.document.isolated.push_back(std::move(block));
return true;
}
const std::string_view value = session.peek().value;
if (detail::isTopLevelStarter(value) || detail::isEmbeddedInNode(value) ||
isKeyword(value, "isolated")) {
session.fail();
stream.rewind(anchor);
return false;
}
session.take();
}
session.fail();
stream.rewind(anchor);
return false;
}
} // namespace eu07::scene::isolated

View File

@@ -0,0 +1,57 @@
#pragma once
#include <eu07/scene/match.hpp>
#include <array>
#include <string_view>
namespace eu07::scene {
inline constexpr std::array<std::string_view, 30> kScenarioDirectiveKeywords{{
"atmo",
"sky",
"time",
"FirstInit",
"trainset",
"endtrainset",
"assignment",
"endassignment",
"event",
"include",
"camera",
"config",
"lua",
"node",
"origin",
"orgin",
"endorigin",
"scale",
"endscale",
"rotate",
"group",
"endgroup",
"isolated",
"area",
"terrain",
"description",
"light",
"test",
"endtest",
}};
[[nodiscard]] inline bool isScenarioDirectiveKeyword(const std::string_view token) noexcept {
for (const std::string_view keyword : kScenarioDirectiveKeywords) {
if (keyword == "FirstInit") {
if (isKeywordIgnoreCase(token, keyword)) {
return true;
}
continue;
}
if (isKeyword(token, keyword)) {
return true;
}
}
return false;
}
} // namespace eu07::scene

View File

@@ -0,0 +1,35 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_light
// light location ambient diffuse specular endlight
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::light {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "light")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
while (!session.empty()) {
if (session.at("endlight")) {
session.take();
break;
}
session.take();
}
ctx.document.light.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::light

View File

@@ -0,0 +1,31 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_lua
// lua file
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/parse.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/match.hpp>
namespace eu07::scene::lua {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "lua")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
const std::size_t anchor = stream.checkpoint();
detail::ParseSession session(stream, block, anchor);
if (!session.empty()) {
session.take();
}
ctx.document.lua.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::lua

View File

@@ -0,0 +1,33 @@
#pragma once
#include <cctype>
#include <string_view>
namespace eu07::scene {
[[nodiscard]] inline bool asciiEquals(std::string_view a, std::string_view b) noexcept {
return a == b;
}
[[nodiscard]] inline bool asciiEqualsIgnoreCase(std::string_view a, std::string_view b) noexcept {
if (a.size() != b.size()) {
return false;
}
for (std::size_t i = 0; i < a.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(a[i])) !=
std::tolower(static_cast<unsigned char>(b[i]))) {
return false;
}
}
return true;
}
[[nodiscard]] inline bool isKeyword(std::string_view token, std::string_view keyword) noexcept {
return asciiEquals(token, keyword);
}
[[nodiscard]] inline bool isKeywordIgnoreCase(std::string_view token, std::string_view keyword) noexcept {
return asciiEqualsIgnoreCase(token, keyword);
}
} // namespace eu07::scene

View File

@@ -0,0 +1,66 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/node/common.hpp>
#include <eu07/scene/node/kinds.hpp>
#include <eu07/scene/node/shared.hpp>
#include <eu07/scene/scratch.hpp>
#include <string>
#include <vector>
namespace eu07::scene::node {
inline void applyScratchContext(NodeHeader& header, const SceneScratchpad& scratch) {
header.originOffset = scratch.currentOrigin();
header.scaleFactor = scratch.currentScale();
header.rotation = scratch.location.rotation;
header.originStack = scratch.location.originStack;
header.scaleStack = scratch.location.scaleStack;
header.groupStackDepth = scratch.group.activeStack.size();
header.groupHandle = scratch.activeGroupHandle();
if (scratch.trainset.isOpen && scratch.trainset.docIndex) {
header.trainsetIndex = *scratch.trainset.docIndex;
header.trainset = TrainsetContext{
scratch.trainset.name,
scratch.trainset.track,
scratch.trainset.offset,
scratch.trainset.velocity,
};
}
}
[[nodiscard]] inline bool parseInto(TokenStream& stream, ParseContext& context) {
const std::size_t anchor = stream.checkpoint();
NodeHeader header;
std::string subtype;
std::vector<SourceToken> raw;
if (!io::consumeHeader(stream, header, subtype, raw)) {
return false;
}
applyScratchContext(header, context.scratch);
const io::NodeKindDesc* const kind = io::findKind(subtype, io::kAllKinds);
if (kind == nullptr) {
stream.rewind(anchor);
return false;
}
if (!kind->parseAndStore(stream, context.document, header, nullptr)) {
stream.rewind(anchor);
return false;
}
return true;
}
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& context) {
return parseInto(stream, context);
}
} // namespace eu07::scene::node

View File

@@ -0,0 +1,60 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/detail/subtype.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <string_view>
#include <vector>
namespace eu07::scene {
struct SceneDocument;
} // namespace eu07::scene
namespace eu07::scene::node::io {
struct NodeKindDesc {
std::string_view subtype;
std::string_view endMarker;
bool (*parseAndStore)(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw);
};
[[nodiscard]] inline bool consumeHeader(
TokenStream& stream,
NodeHeader& header,
std::string& subtypeOut,
std::vector<SourceToken>& raw) {
if (stream.empty() || !isKeyword(stream.peek().value, "node")) {
return false;
}
SourceToken head;
if (!takeToken(stream, raw, head)) {
return false;
}
header.line = head.sourceLine;
if (!takeDouble(stream, raw, header.rangeMax) || !takeDouble(stream, raw, header.rangeMin) ||
!takeString(stream, raw, header.name)) {
return false;
}
if (stream.empty() || !eu07::scene::detail::isNodeSubtype(stream.peek().value)) {
return false;
}
if (!takeString(stream, raw, subtypeOut)) {
return false;
}
return true;
}
} // namespace eu07::scene::node::io

View File

@@ -0,0 +1,128 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node::dynamic
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/shared.hpp>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeDynamic {
NodeHeader header;
std::string datafolder;
std::string skinfile;
std::string mmdfile;
std::string pathname;
double vehicleOffset = -1.0;
std::string driverType;
CouplingSpec coupling;
double velocity = 0.0;
int loadcount = 0;
std::optional<std::string> loadtype;
std::optional<std::string> loadModifier;
std::optional<std::string> destination;
bool inTrainset = false;
std::vector<SourceToken> raw;
};
namespace node_dynamic {
inline constexpr std::string_view kSubtype = "dynamic";
inline constexpr std::string_view kEndMarker = "enddynamic";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeDynamic& out) {
out.header = header;
out.raw.clear();
out.inTrainset = header.trainset.has_value();
if (!node::io::takeString(stream, raw, out.datafolder) ||
!node::io::takeString(stream, raw, out.skinfile) ||
!node::io::takeString(stream, raw, out.mmdfile)) {
return false;
}
if (out.inTrainset) {
out.pathname = header.trainset->track;
} else if (!node::io::takeString(stream, raw, out.pathname)) {
return false;
}
if (!node::io::takeDouble(stream, raw, out.vehicleOffset) ||
!node::io::takeString(stream, raw, out.driverType)) {
return false;
}
std::string couplingRaw;
if (out.inTrainset) {
if (!node::io::takeString(stream, raw, couplingRaw)) {
return false;
}
} else {
couplingRaw = "3";
}
out.coupling = parseCouplingSpec(couplingRaw);
if (out.inTrainset) {
out.velocity = header.trainset->velocity;
} else if (!node::io::takeDouble(stream, raw, out.velocity)) {
return false;
}
double loadcount = 0.0;
if (!node::io::takeDouble(stream, raw, loadcount)) {
return false;
}
out.loadcount = static_cast<int>(loadcount);
if (out.loadcount > 0) {
if (stream.empty()) {
return false;
}
if (isKeyword(stream.peek().value, kEndMarker)) {
out.loadcount = 0;
} else {
std::string loadtype;
if (!node::io::takeString(stream, raw, loadtype)) {
return false;
}
if (isKeyword(loadtype, kEndMarker)) {
out.loadcount = 0;
} else {
out.loadtype = std::move(loadtype);
}
}
}
if (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
std::string token;
if (!node::io::takeString(stream, raw, token)) {
return false;
}
if (!isKeyword(token, kEndMarker)) {
std::string dest;
if (!node::io::takeString(stream, raw, dest)) {
return false;
}
if (!isKeyword(dest, kEndMarker)) {
out.destination = std::move(dest);
}
}
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_dynamic
} // namespace eu07::scene

View File

@@ -0,0 +1,169 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node::eventlauncher
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct EventLauncherCondition {
std::string memcell;
std::string parameter;
std::optional<double> value1;
std::optional<double> value2;
int checkMask = 0;
};
struct ParsedNodeEventlauncher {
NodeHeader header;
Vec3 position;
double radius = 0.0;
std::string key;
std::string time;
std::string event1;
std::optional<std::string> event2;
std::optional<EventLauncherCondition> condition;
bool trainTriggered = false;
std::vector<SourceToken> raw;
};
namespace node_eventlauncher {
inline constexpr std::string_view kSubtype = "eventlauncher";
inline constexpr std::string_view kEndMarker = "endeventlauncher";
[[nodiscard]] inline bool takeConditionField(
TokenStream& stream,
std::vector<SourceToken>& raw,
const int flag,
std::optional<double>& out,
int& checkMask) {
std::string token;
if (!node::io::takeString(stream, raw, token)) {
return false;
}
if (token == "*") {
return true;
}
checkMask |= flag;
const std::optional<double> value = node::io::parseDouble(token);
if (!value) {
return false;
}
out = *value;
return true;
}
[[nodiscard]] inline bool consumeLauncherEnd(TokenStream& stream, std::vector<SourceToken>& raw) {
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeEventlauncher& out) {
out.header = header;
out.raw.clear();
if (!node::io::takeVec3(stream, raw, out.position) ||
!node::io::takeDouble(stream, raw, out.radius) || !node::io::takeString(stream, raw, out.key) ||
!node::io::takeString(stream, raw, out.time) || !node::io::takeString(stream, raw, out.event1)) {
return false;
}
if (stream.empty() || node::io::atEnd(stream, kSubtype, kEndMarker)) {
return consumeLauncherEnd(stream, raw);
}
std::string token;
if (!node::io::takeString(stream, raw, token)) {
return false;
}
// MaSzyna (EvLaunch.cpp): "end" zamiast drugiego eventu lub endeventlauncher.
if (isKeyword(token, "end") || isKeyword(token, "endeventlauncher")) {
if (isKeyword(token, "endeventlauncher")) {
return consumeLauncherEnd(stream, raw);
}
return true;
}
if (!isKeyword(token, "condition") && !isKeyword(token, "traintriggered")) {
out.event2 = token;
if (stream.empty() || node::io::atEnd(stream, kSubtype, kEndMarker)) {
return consumeLauncherEnd(stream, raw);
}
if (!node::io::takeString(stream, raw, token)) {
return false;
}
if (isKeyword(token, "end") || isKeyword(token, "endeventlauncher")) {
if (isKeyword(token, "endeventlauncher")) {
return consumeLauncherEnd(stream, raw);
}
return true;
}
}
if (isKeyword(token, "condition")) {
EventLauncherCondition cond;
if (!node::io::takeString(stream, raw, cond.memcell) ||
!node::io::takeString(stream, raw, cond.parameter)) {
return false;
}
if (cond.parameter != "*") {
cond.checkMask |= 1;
}
if (!takeConditionField(stream, raw, 2, cond.value1, cond.checkMask)) {
return false;
}
if (!takeConditionField(stream, raw, 4, cond.value2, cond.checkMask)) {
return false;
}
if (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
std::string closing;
if (!node::io::takeString(stream, raw, closing)) {
return false;
}
token = std::move(closing);
}
out.condition = std::move(cond);
if (isKeyword(token, "end")) {
return true;
}
}
if (isKeyword(token, "traintriggered")) {
out.trainTriggered = true;
if (stream.empty()) {
return true;
}
if (node::io::atEnd(stream, kSubtype, kEndMarker)) {
return consumeLauncherEnd(stream, raw);
}
if (!node::io::takeString(stream, raw, token)) {
return false;
}
if (isKeyword(token, "end")) {
return true;
}
}
if (stream.empty()) {
return true;
}
return consumeLauncherEnd(stream, raw);
}
} // namespace node_eventlauncher
} // namespace eu07::scene

View File

@@ -0,0 +1,31 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
#include <eu07/scene/node/shared.hpp>
#include <eu07/scene/node/types.hpp>
namespace eu07::scene {
struct NodeHeader {
std::size_t line = 0;
double rangeMax;
double rangeMin;
std::string name;
Vec3 originOffset{};
Vec3 scaleFactor{1.0, 1.0, 1.0};
Vec3 rotation{};
std::vector<Vec3> originStack;
std::vector<Vec3> scaleStack;
std::size_t groupStackDepth = 0;
std::optional<std::size_t> groupHandle;
std::optional<std::size_t> trainsetIndex;
std::optional<TrainsetContext> trainset;
};
} // namespace eu07::scene

View File

@@ -0,0 +1,295 @@
#pragma once
#include <eu07/scene/document.hpp>
#include <eu07/scene/node/common.hpp>
#include <eu07/scene/node/dynamic.hpp>
#include <eu07/scene/node/eventlauncher.hpp>
#include <eu07/scene/node/line_loop.hpp>
#include <eu07/scene/node/line_strip.hpp>
#include <eu07/scene/node/lines.hpp>
#include <eu07/scene/node/memcell.hpp>
#include <eu07/scene/node/model.hpp>
#include <eu07/scene/node/sound.hpp>
#include <eu07/scene/node/track.hpp>
#include <eu07/scene/node/traction.hpp>
#include <eu07/scene/node/traction_power.hpp>
#include <eu07/scene/node/triangle_fan.hpp>
#include <eu07/scene/node/triangle_strip.hpp>
#include <eu07/scene/node/triangles.hpp>
#include <array>
#include <span>
#include <vector>
namespace eu07::scene::node::io {
inline void storeDynamic(SceneDocument& document, ParsedNodeDynamic&& node) {
document.nodeDynamic.push_back(std::move(node));
}
inline void storeModel(SceneDocument& document, ParsedNodeModel&& node) {
document.nodeModel.push_back(std::move(node));
}
inline bool parseTrack(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
std::vector<SourceToken> raw;
std::string kindToken;
if (!node::io::takeString(stream, raw, kindToken)) {
return false;
}
auto finalize = [&](auto& node) {
node.raw = std::move(raw);
if (embedRaw != nullptr) {
embedRaw->insert(embedRaw->end(), node.raw.begin(), node.raw.end());
}
};
if (node_track::isNormalLike(kindToken)) {
ParsedTrackNormal node;
if (!node_track::parseNormalBody(stream, raw, header, kindToken, node)) {
return false;
}
finalize(node);
document.nodeTrackNormal.push_back(std::move(node));
return true;
}
if (isKeyword(kindToken, "switch")) {
ParsedTrackSwitch node;
if (!node_track::parseSwitchBody(stream, raw, header, node)) {
return false;
}
finalize(node);
document.nodeTrackSwitch.push_back(std::move(node));
return true;
}
if (isKeyword(kindToken, "road")) {
ParsedTrackRoad node;
if (!node_track::parseRoadBody(stream, raw, header, node)) {
return false;
}
finalize(node);
document.nodeTrackRoad.push_back(std::move(node));
return true;
}
if (isKeyword(kindToken, "cross")) {
ParsedTrackCross node;
if (!node_track::parseCrossBody(stream, raw, header, node)) {
return false;
}
finalize(node);
document.nodeTrackCross.push_back(std::move(node));
return true;
}
ParsedTrackOther node;
if (!node_track::parseOtherBody(
stream, raw, header, kindToken, node, node_track::isRiverLike(kindToken))) {
return false;
}
finalize(node);
document.nodeTrackOther.push_back(std::move(node));
return true;
}
inline void storeTraction(SceneDocument& document, ParsedNodeTraction&& node) {
document.nodeTraction.push_back(std::move(node));
}
inline void storeTractionPower(SceneDocument& document, ParsedNodeTractionPowerSource&& node) {
document.nodeTractionPower.push_back(std::move(node));
}
inline void storeTriangles(SceneDocument& document, ParsedNodeTriangles&& node) {
document.nodeTriangles.push_back(std::move(node));
}
inline void storeTriangleStrip(SceneDocument& document, ParsedNodeTriangleStrip&& node) {
document.nodeTriangleStrip.push_back(std::move(node));
}
inline void storeTriangleFan(SceneDocument& document, ParsedNodeTriangleFan&& node) {
document.nodeTriangleFan.push_back(std::move(node));
}
inline void storeLines(SceneDocument& document, ParsedNodeLines&& node) {
document.nodeLines.push_back(std::move(node));
}
inline void storeLineStrip(SceneDocument& document, ParsedNodeLineStrip&& node) {
document.nodeLineStrip.push_back(std::move(node));
}
inline void storeLineLoop(SceneDocument& document, ParsedNodeLineLoop&& node) {
document.nodeLineLoop.push_back(std::move(node));
}
inline void storeMemcell(SceneDocument& document, ParsedNodeMemcell&& node) {
document.nodeMemcell.push_back(std::move(node));
}
inline void storeEventlauncher(SceneDocument& document, ParsedNodeEventlauncher&& node) {
document.nodeEventlauncher.push_back(std::move(node));
}
inline void storeSound(SceneDocument& document, ParsedNodeSound&& node) {
document.nodeSound.push_back(std::move(node));
}
template <typename Parsed, bool (*ParseBody)(TokenStream&, std::vector<SourceToken>&, const NodeHeader&, Parsed&), void (*Store)(SceneDocument&, Parsed&&)>
[[nodiscard]] inline bool parseKind(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
std::vector<SourceToken> raw;
Parsed parsed;
if (!ParseBody(stream, raw, header, parsed)) {
return false;
}
parsed.raw = std::move(raw);
if (embedRaw != nullptr) {
embedRaw->insert(embedRaw->end(), parsed.raw.begin(), parsed.raw.end());
}
Store(document, std::move(parsed));
return true;
}
inline bool parseDynamic(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeDynamic, node_dynamic::parseBody, storeDynamic>(
stream, document, header, embedRaw);
}
inline bool parseModel(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeModel, node_model::parseBody, storeModel>(
stream, document, header, embedRaw);
}
inline bool parseTraction(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeTraction, node_traction::parseBody, storeTraction>(
stream, document, header, embedRaw);
}
inline bool parseTractionPower(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeTractionPowerSource, node_traction_power::parseBody, storeTractionPower>(
stream, document, header, embedRaw);
}
inline bool parseTriangles(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeTriangles, node_triangles::parseBody, storeTriangles>(
stream, document, header, embedRaw);
}
inline bool parseTriangleStrip(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeTriangleStrip, node_triangle_strip::parseBody, storeTriangleStrip>(
stream, document, header, embedRaw);
}
inline bool parseTriangleFan(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeTriangleFan, node_triangle_fan::parseBody, storeTriangleFan>(
stream, document, header, embedRaw);
}
inline bool parseLines(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeLines, node_lines::parseBody, storeLines>(
stream, document, header, embedRaw);
}
inline bool parseLineStrip(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeLineStrip, node_line_strip::parseBody, storeLineStrip>(
stream, document, header, embedRaw);
}
inline bool parseLineLoop(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeLineLoop, node_line_loop::parseBody, storeLineLoop>(
stream, document, header, embedRaw);
}
inline bool parseMemcell(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeMemcell, node_memcell::parseBody, storeMemcell>(
stream, document, header, embedRaw);
}
inline bool parseEventlauncher(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeEventlauncher, node_eventlauncher::parseBody, storeEventlauncher>(
stream, document, header, embedRaw);
}
inline bool parseSound(
TokenStream& stream,
SceneDocument& document,
const NodeHeader& header,
std::vector<SourceToken>* embedRaw) {
return parseKind<ParsedNodeSound, node_sound::parseBody, storeSound>(
stream, document, header, embedRaw);
}
inline constexpr std::array<NodeKindDesc, 14> kAllKinds{{
{node_dynamic::kSubtype, node_dynamic::kEndMarker, &parseDynamic},
{node_model::kSubtype, node_model::kEndMarker, &parseModel},
{node_track::kSubtype, node_track::kEndMarker, &parseTrack},
{node_traction::kSubtype, node_traction::kEndMarker, &parseTraction},
{node_traction_power::kSubtype, node_traction_power::kEndMarker, &parseTractionPower},
{node_triangles::kSubtype, node_triangles::kEndMarker, &parseTriangles},
{node_triangle_strip::kSubtype, node_triangle_strip::kEndMarker, &parseTriangleStrip},
{node_triangle_fan::kSubtype, node_triangle_fan::kEndMarker, &parseTriangleFan},
{node_lines::kSubtype, node_lines::kEndMarker, &parseLines},
{node_line_strip::kSubtype, node_line_strip::kEndMarker, &parseLineStrip},
{node_line_loop::kSubtype, node_line_loop::kEndMarker, &parseLineLoop},
{node_memcell::kSubtype, node_memcell::kEndMarker, &parseMemcell},
{node_eventlauncher::kSubtype, node_eventlauncher::kEndMarker, &parseEventlauncher},
{node_sound::kSubtype, node_sound::kEndMarker, &parseSound},
}};
[[nodiscard]] inline const NodeKindDesc* findKind(
const std::string_view subtype,
const std::span<const NodeKindDesc> kinds) noexcept {
for (const NodeKindDesc& kind : kinds) {
if (isKeyword(subtype, kind.subtype)) {
return &kind;
}
}
return nullptr;
}
} // namespace eu07::scene::node::io

View File

@@ -0,0 +1,53 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <vector>
namespace eu07::scene {
struct ParsedNodeLineLoop {
NodeHeader header;
MaterialRgb color;
double thickness = 0.0;
std::vector<Vec3> points;
std::vector<SourceToken> raw;
};
namespace node_line_loop {
inline constexpr std::string_view kSubtype = "line_loop";
inline constexpr std::string_view kEndMarker = "endline_loop";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeLineLoop& out) {
out.header = header;
out.raw.clear();
if (!node::io::takeDouble(stream, raw, out.color.r) ||
!node::io::takeDouble(stream, raw, out.color.g) ||
!node::io::takeDouble(stream, raw, out.color.b) ||
!node::io::takeDouble(stream, raw, out.thickness)) {
return false;
}
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
Vec3 point;
if (!node::io::takeVec3(stream, raw, point)) {
return false;
}
out.points.push_back(point);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_line_loop
} // namespace eu07::scene

View File

@@ -0,0 +1,53 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <vector>
namespace eu07::scene {
struct ParsedNodeLineStrip {
NodeHeader header;
MaterialRgb color;
double thickness = 0.0;
std::vector<Vec3> points;
std::vector<SourceToken> raw;
};
namespace node_line_strip {
inline constexpr std::string_view kSubtype = "line_strip";
inline constexpr std::string_view kEndMarker = "endline_strip";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeLineStrip& out) {
out.header = header;
out.raw.clear();
if (!node::io::takeDouble(stream, raw, out.color.r) ||
!node::io::takeDouble(stream, raw, out.color.g) ||
!node::io::takeDouble(stream, raw, out.color.b) ||
!node::io::takeDouble(stream, raw, out.thickness)) {
return false;
}
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
Vec3 point;
if (!node::io::takeVec3(stream, raw, point)) {
return false;
}
out.points.push_back(point);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_line_strip
} // namespace eu07::scene

View File

@@ -0,0 +1,61 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node (lines)
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <utility>
#include <vector>
namespace eu07::scene {
struct LineSegment {
Vec3 a;
Vec3 b;
};
struct ParsedNodeLines {
NodeHeader header;
MaterialRgb color;
double thickness = 0.0;
std::vector<LineSegment> segments;
std::vector<SourceToken> raw;
};
namespace node_lines {
inline constexpr std::string_view kSubtype = "lines";
inline constexpr std::string_view kEndMarker = "endlines";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeLines& out) {
out.header = header;
out.raw.clear();
if (!node::io::takeDouble(stream, raw, out.color.r) ||
!node::io::takeDouble(stream, raw, out.color.g) ||
!node::io::takeDouble(stream, raw, out.color.b) ||
!node::io::takeDouble(stream, raw, out.thickness)) {
return false;
}
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
LineSegment segment;
if (!node::io::takeVec3(stream, raw, segment.a) || !node::io::takeVec3(stream, raw, segment.b)) {
return false;
}
out.segments.push_back(segment);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_lines
} // namespace eu07::scene

View File

@@ -0,0 +1,56 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeMemcell {
NodeHeader header;
Vec3 position;
std::string command;
double value1 = 0.0;
double value2 = 0.0;
std::optional<std::string> trackName;
std::vector<SourceToken> raw;
};
namespace node_memcell {
inline constexpr std::string_view kSubtype = "memcell";
inline constexpr std::string_view kEndMarker = "endmemcell";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeMemcell& out) {
out.header = header;
out.raw.clear();
std::string trackRaw;
if (!node::io::takeVec3(stream, raw, out.position) ||
!node::io::takeString(stream, raw, out.command) ||
!node::io::takeDouble(stream, raw, out.value1) ||
!node::io::takeDouble(stream, raw, out.value2) ||
!node::io::takeString(stream, raw, trackRaw)) {
return false;
}
if (!isKeyword(trackRaw, "none")) {
out.trackName = std::move(trackRaw);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_memcell
} // namespace eu07::scene

View File

@@ -0,0 +1,45 @@
#pragma once
// Wspolne parsowanie siatki: triangle_strip, triangle_fan
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/triangles.hpp>
#include <eu07/scene/node/types.hpp>
#include <string>
#include <string_view>
#include <vector>
namespace eu07::scene::node::io {
template <typename Parsed>
[[nodiscard]] inline bool parseTexturedMeshBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
const std::string_view subtype,
const std::string_view endMarker,
Parsed& out) {
out.header = header;
out.raw.clear();
if (!takeString(stream, raw, out.texture)) {
return false;
}
while (!stream.empty() && !atEnd(stream, subtype, endMarker)) {
MeshVertex vertex;
const bool hasMore = stream.remaining() > 1;
if (!node_triangles::takeVertex(stream, raw, vertex, hasMore)) {
return false;
}
out.vertices.push_back(vertex);
}
return consumeEnd(stream, raw, subtype, endMarker);
}
} // namespace eu07::scene::node::io

View File

@@ -0,0 +1,175 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node::model
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeModel {
NodeHeader header;
Vec3 location;
double rotationY = 0.0;
std::string modelPath;
std::string replaceableSkin;
std::optional<std::vector<double>> lightStates;
std::optional<Vec3> angles;
std::optional<std::vector<MaterialRgb>> lightColors;
std::optional<Vec3> inlineScale;
bool noTransition = false;
std::vector<std::string> extra;
std::vector<SourceToken> raw;
};
namespace node_model {
inline constexpr std::string_view kSubtype = "model";
inline constexpr std::string_view kEndMarker = "endmodel";
[[nodiscard]] inline bool takeLightColor(
TokenStream& stream,
std::vector<SourceToken>& raw,
MaterialRgb& color) {
SourceToken token;
if (!node::io::takeToken(stream, raw, token)) {
return false;
}
if (const std::optional<double> value = node::io::parseDouble(token.value)) {
color.r = *value;
SourceToken g;
SourceToken b;
if (!node::io::takeToken(stream, raw, g) || !node::io::takeToken(stream, raw, b)) {
return false;
}
if (const std::optional<double> gv = node::io::parseDouble(g.value)) {
color.g = *gv;
}
if (const std::optional<double> bv = node::io::parseDouble(b.value)) {
color.b = *bv;
}
return true;
}
try {
const int packed = std::stoi(token.value, nullptr, 16);
color.r = static_cast<double>((packed >> 16) & 0xFF);
color.g = static_cast<double>((packed >> 8) & 0xFF);
color.b = static_cast<double>(packed & 0xFF);
return true;
} catch (...) {
return false;
}
}
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeModel& out) {
out.header = header;
out.raw.clear();
if (!node::io::takeVec3(stream, raw, out.location) ||
!node::io::takeDouble(stream, raw, out.rotationY) ||
!node::io::takeString(stream, raw, out.modelPath) ||
!node::io::takeString(stream, raw, out.replaceableSkin)) {
return false;
}
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
const std::string_view value = stream.peek().value;
if (isKeyword(value, "lights")) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
std::vector<double> states;
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
const std::string_view next = stream.peek().value;
if (isKeyword(next, "angles") || isKeyword(next, "lightcolors") ||
isKeyword(next, "notransition") || isKeyword(next, "scale")) {
break;
}
double state = 0.0;
if (!node::io::takeDouble(stream, raw, state)) {
break;
}
states.push_back(state);
}
out.lightStates = std::move(states);
continue;
}
if (isKeyword(value, "angles")) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
Vec3 angles;
if (!node::io::takeVec3(stream, raw, angles)) {
return false;
}
out.angles = angles;
continue;
}
if (isKeyword(value, "lightcolors")) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
std::vector<MaterialRgb> colors;
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
const std::string_view next = stream.peek().value;
if (isKeyword(next, "notransition") || isKeyword(next, "scale")) {
break;
}
MaterialRgb color;
if (!takeLightColor(stream, raw, color)) {
break;
}
colors.push_back(color);
}
out.lightColors = std::move(colors);
continue;
}
if (isKeyword(value, "scale")) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
Vec3 scale;
if (!node::io::takeVec3(stream, raw, scale)) {
return false;
}
out.inlineScale = scale;
continue;
}
if (isKeyword(value, "notransition")) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
out.noTransition = true;
continue;
}
SourceToken token;
if (!node::io::takeToken(stream, raw, token)) {
break;
}
out.extra.push_back(token.value);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_model
} // namespace eu07::scene

View File

@@ -0,0 +1,165 @@
#pragma once
#include <eu07/parser.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/include_resolve.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/types.hpp>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace eu07::scene::node::io {
[[nodiscard]] inline bool isEndMarkerFor(
const std::string_view token,
const std::string_view subtype,
const std::string_view endMarker) noexcept {
if (isKeyword(token, endMarker)) {
return true;
}
if (isKeyword(subtype, "triangle_strip") && isKeyword(token, "endtri")) {
return true;
}
if (isKeyword(subtype, "triangles") &&
(isKeyword(token, "endtri") || isKeyword(token, "endtriangles"))) {
return true;
}
if (isKeyword(subtype, "triangle_fan") && isKeyword(token, "endtri")) {
return true;
}
if (isKeyword(subtype, "lines") &&
(isKeyword(token, "endlines") || isKeyword(token, "endline"))) {
return true;
}
if (isKeyword(subtype, "line_strip") &&
(isKeyword(token, "endline_strip") || isKeyword(token, "endline"))) {
return true;
}
if (isKeyword(subtype, "line_loop") &&
(isKeyword(token, "endline_loop") || isKeyword(token, "endline"))) {
return true;
}
if (isKeyword(subtype, "eventlauncher") &&
(isKeyword(token, "end") || isKeyword(token, endMarker))) {
return true;
}
return false;
}
inline void appendRaw(std::vector<SourceToken>& raw, const SourceToken& token) {
raw.push_back(token);
}
[[nodiscard]] inline bool isMissingNumericToken(const std::string_view text) noexcept {
return text.empty() || isKeywordIgnoreCase(text, "none") ||
isKeywordIgnoreCase(text, "undefined");
}
[[nodiscard]] inline std::optional<double> parseDouble(const std::string_view text) {
if (isMissingNumericToken(text)) {
return 0.0;
}
try {
std::size_t used = 0;
const double value = std::stod(std::string(text), &used);
if (used != text.size()) {
return std::nullopt;
}
return value;
} catch (...) {
return std::nullopt;
}
}
[[nodiscard]] inline bool takeToken(
TokenStream& stream,
std::vector<SourceToken>& raw,
SourceToken& out) {
if (stream.empty()) {
return false;
}
out = stream.consume();
appendRaw(raw, out);
return true;
}
[[nodiscard]] inline bool takeString(
TokenStream& stream,
std::vector<SourceToken>& raw,
std::string& out) {
SourceToken token;
if (!takeToken(stream, raw, token)) {
return false;
}
out = token.value;
return true;
}
[[nodiscard]] inline bool takeDouble(
TokenStream& stream,
std::vector<SourceToken>& raw,
double& out) {
SourceToken token;
if (!takeToken(stream, raw, token)) {
return false;
}
const std::optional<double> value = parseDouble(token.value);
if (!value) {
return false;
}
out = *value;
return true;
}
[[nodiscard]] inline bool takeDoubleOrPlaceholder(
TokenStream& stream,
std::vector<SourceToken>& raw,
double& out) {
SourceToken token;
if (!takeToken(stream, raw, token)) {
return false;
}
if (const std::optional<double> value = parseDouble(token.value)) {
out = *value;
return true;
}
std::uint8_t paramIndex = 0;
if (detail::parseIncludeParameterIndex(token.value, paramIndex)) {
out = 0.0;
return true;
}
return false;
}
[[nodiscard]] inline bool takeVec3(
TokenStream& stream,
std::vector<SourceToken>& raw,
Vec3& out) {
return takeDouble(stream, raw, out.x) && takeDouble(stream, raw, out.y) &&
takeDouble(stream, raw, out.z);
}
[[nodiscard]] inline bool atEnd(
TokenStream& stream,
const std::string_view subtype,
const std::string_view endMarker) {
return !stream.empty() && isEndMarkerFor(stream.peek().value, subtype, endMarker);
}
[[nodiscard]] inline bool consumeEnd(
TokenStream& stream,
std::vector<SourceToken>& raw,
const std::string_view subtype,
const std::string_view endMarker) {
if (!atEnd(stream, subtype, endMarker)) {
return false;
}
SourceToken token;
return takeToken(stream, raw, token);
}
} // namespace eu07::scene::node::io

View File

@@ -0,0 +1,74 @@
#pragma once
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/types.hpp>
namespace eu07::scene {
enum class NodeVisibilityMode {
Visible,
Hidden,
};
enum class TractionMaterialKind {
Copper,
Aluminum,
None,
Raw,
};
struct TrainsetContext {
std::string name;
std::string track;
double consistOffset = 0.0;
double velocity = 0.0;
};
struct CouplingSpec {
std::string raw = "3";
int type = 3;
std::string params;
};
[[nodiscard]] inline CouplingSpec parseCouplingSpec(const std::string& raw) {
CouplingSpec out;
out.raw = raw;
const std::size_t dot = raw.find('.');
const std::string typeToken = dot == std::string::npos ? raw : raw.substr(0, dot);
try {
out.type = std::stoi(typeToken);
} catch (...) {
out.type = 3;
}
if (dot != std::string::npos) {
out.params = raw.substr(dot + 1);
}
return out;
}
[[nodiscard]] inline TractionMaterialKind parseTractionMaterial(const std::string_view token) {
if (isKeyword(token, "none")) {
return TractionMaterialKind::None;
}
if (isKeyword(token, "al")) {
return TractionMaterialKind::Aluminum;
}
if (isKeyword(token, "cu") || isKeyword(token, "miedz") || isKeyword(token, "miedź")) {
return TractionMaterialKind::Copper;
}
return TractionMaterialKind::Raw;
}
[[nodiscard]] inline NodeVisibilityMode parseNodeVisibility(const std::string_view token) noexcept {
if (isKeyword(token, "vis")) {
return NodeVisibilityMode::Visible;
}
return NodeVisibilityMode::Hidden;
}
} // namespace eu07::scene

View File

@@ -0,0 +1,40 @@
#pragma once
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeSound {
NodeHeader header;
Vec3 position;
std::string wavFile;
std::vector<SourceToken> raw;
};
namespace node_sound {
inline constexpr std::string_view kSubtype = "sound";
inline constexpr std::string_view kEndMarker = "endsound";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeSound& out) {
out.header = header;
out.raw.clear();
return node::io::takeVec3(stream, raw, out.position) &&
node::io::takeString(stream, raw, out.wavFile) &&
node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_sound
} // namespace eu07::scene

View File

@@ -0,0 +1,736 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node::track
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace eu07::scene {
enum class TrackVisibilityMode {
Visible,
Hidden,
};
struct TrackBezier {
Vec3 p1;
double roll1 = 0.0;
Vec3 cv1;
Vec3 cv2;
Vec3 p2;
double roll2 = 0.0;
double radius = 0.0;
};
struct TrackSleeperModel {
double spacing = 0.0;
std::string model;
std::string texture;
double offsetLateral = 0.0;
double offsetAlong = 0.0;
double offsetVertical = 0.0;
double bedLowering = 0.0;
};
struct TrackEvents {
std::optional<std::string> occupiedStop;
std::optional<std::string> occupiedEnterP1;
std::optional<std::string> occupiedEnterP2;
std::optional<std::string> anyStop;
std::optional<std::string> anyEnterP1;
std::optional<std::string> anyEnterP2;
};
struct TrackOptionals {
std::optional<double> velocity;
std::optional<double> overhead;
std::optional<std::string> railProfile;
std::optional<std::string> trackBed;
std::optional<double> frictionOverride;
std::optional<double> verticalRadius;
std::optional<double> angle1;
std::optional<double> angle2;
std::optional<std::string> fouling1;
std::optional<std::string> fouling2;
std::optional<TrackSleeperModel> sleeperModel;
TrackEvents events;
std::vector<std::string> isolated;
std::vector<std::pair<std::string, std::string>> extraKeywords;
};
// normal / turn / table — szyny + podsypka
struct NormalRailVisibility {
std::string railTexture;
double railTexLength = 0.0;
std::string ballastTexture;
double ballastHeight = 0.0;
double ballastWidth = 0.0;
double ballastSlope = 0.0;
};
struct ParsedTrackNormal {
NodeHeader header;
std::string trajectoryKind;
double length = 0.0;
double gauge = 0.0;
double friction = 0.0;
double soundDist = 0.0;
int quality = 0;
int damageFlag = 0;
std::string environment;
TrackVisibilityMode visibilityMode = TrackVisibilityMode::Hidden;
std::optional<NormalRailVisibility> visibility;
std::vector<TrackBezier> beziers;
TrackOptionals optionals;
std::vector<SourceToken> raw;
};
// switch — szyny toru zasadniczego + toru zwrotnego
struct SwitchRailVisibility {
std::string mainRailTexture;
double mainRailTexLength = 0.0;
std::string switchRailTexture;
double unusedHeight = 0.0;
double unusedWidth = 0.0;
double unusedSlope = 0.0;
};
struct ParsedTrackSwitch {
NodeHeader header;
double length = 0.0;
double gauge = 0.0;
double friction = 0.0;
double soundDist = 0.0;
int quality = 0;
int damageFlag = 0;
std::string environment;
TrackVisibilityMode visibilityMode = TrackVisibilityMode::Hidden;
std::optional<SwitchRailVisibility> visibility;
std::vector<TrackBezier> beziers;
TrackOptionals optionals;
std::vector<SourceToken> raw;
};
// road — nawierzchnia + pobocze/chodnik
struct RoadSurfaceVisibility {
std::string surfaceTexture;
double surfaceTexLength = 0.0;
std::string shoulderTexture;
double shoulderHeight = 0.0;
double shoulderWidth = 0.0;
double shoulderSlope = 0.0;
};
struct ParsedTrackRoad {
NodeHeader header;
double length = 0.0;
double roadWidth = 0.0;
double friction = 0.0;
double soundDist = 0.0;
int quality = 0;
int damageFlag = 0;
std::string environment;
TrackVisibilityMode visibilityMode = TrackVisibilityMode::Hidden;
std::optional<RoadSurfaceVisibility> visibility;
std::vector<TrackBezier> beziers;
TrackOptionals optionals;
std::vector<SourceToken> raw;
};
// cross — jak droga, ale zwykle dwie krzywe
struct ParsedTrackCross {
NodeHeader header;
double length = 0.0;
double roadWidth = 0.0;
double friction = 0.0;
double soundDist = 0.0;
int quality = 0;
int damageFlag = 0;
std::string environment;
TrackVisibilityMode visibilityMode = TrackVisibilityMode::Hidden;
std::optional<RoadSurfaceVisibility> visibility;
std::vector<TrackBezier> beziers;
TrackOptionals optionals;
std::vector<SourceToken> raw;
};
// river / tributary / turn / table / nieznany typ
struct RiverVisibility {
std::string waterTexture;
double waterTexLength = 0.0;
std::string bankTexture;
double bankHeight = 0.0;
double leftBankWidth = 0.0;
double rightBankWidth = 0.0;
};
struct ParsedTrackOther {
NodeHeader header;
std::string trajectoryKind;
double length = 0.0;
double width = 0.0;
double friction = 0.0;
double soundDist = 0.0;
int quality = 0;
int damageFlag = 0;
std::string environment;
TrackVisibilityMode visibilityMode = TrackVisibilityMode::Hidden;
std::optional<RiverVisibility> riverVisibility;
std::optional<NormalRailVisibility> railVisibility;
std::vector<TrackBezier> beziers;
TrackOptionals optionals;
std::vector<SourceToken> raw;
};
namespace node_track {
inline constexpr std::string_view kSubtype = "track";
inline constexpr std::string_view kEndMarker = "endtrack";
struct CoreParams {
double length = 0.0;
double width = 0.0;
double friction = 0.0;
double soundDist = 0.0;
int quality = 0;
int damageFlag = 0;
std::string environment;
TrackVisibilityMode visibilityMode = TrackVisibilityMode::Hidden;
};
[[nodiscard]] inline bool isEnvironment(const std::string_view token) noexcept {
return isKeywordIgnoreCase(token, "flat") || isKeywordIgnoreCase(token, "mountains") ||
isKeywordIgnoreCase(token, "mountain") || isKeywordIgnoreCase(token, "canyon") ||
isKeywordIgnoreCase(token, "tunnel") || isKeywordIgnoreCase(token, "bridge") ||
isKeywordIgnoreCase(token, "bank");
}
[[nodiscard]] inline bool isTrackKeyword(const std::string_view token) noexcept {
return isKeyword(token, "velocity") || isKeyword(token, "isolated") ||
isKeyword(token, "overhead") || isKeyword(token, "railprofile") ||
isKeyword(token, "trackbed") || isKeyword(token, "friction") ||
isKeyword(token, "angle1") || isKeyword(token, "angle2") ||
isKeyword(token, "fouling1") || isKeyword(token, "fouling2") ||
isKeyword(token, "sleepermodel") || isKeyword(token, "vradius") ||
isKeyword(token, "event0") || isKeyword(token, "event1") ||
isKeyword(token, "event2") || isKeyword(token, "eventall0") ||
isKeyword(token, "eventall1") || isKeyword(token, "eventall2");
}
[[nodiscard]] inline Vec3 addVec3(const Vec3& a, const Vec3& b) noexcept {
return {a.x + b.x, a.y + b.y, a.z + b.z};
}
// Wiki MaSzyna: cv1 wzgledem P1, cv2 wzgledem P2.
[[nodiscard]] inline bool takeBezier(
TokenStream& stream,
std::vector<SourceToken>& raw,
TrackBezier& segment) {
if (!node::io::takeVec3(stream, raw, segment.p1) ||
!node::io::takeDouble(stream, raw, segment.roll1) ||
!node::io::takeVec3(stream, raw, segment.cv1) ||
!node::io::takeVec3(stream, raw, segment.cv2) ||
!node::io::takeVec3(stream, raw, segment.p2) ||
!node::io::takeDouble(stream, raw, segment.roll2) ||
!node::io::takeDouble(stream, raw, segment.radius)) {
return false;
}
// cv1/cv2 zostaja wzgledem P1/P2 (wiki MaSzyna) — jak terenAI CubicBezier.
return true;
}
[[nodiscard]] inline bool takeSleeperModel(
TokenStream& stream,
std::vector<SourceToken>& raw,
TrackSleeperModel& out) {
return node::io::takeDouble(stream, raw, out.spacing) &&
node::io::takeString(stream, raw, out.model) &&
node::io::takeString(stream, raw, out.texture) &&
node::io::takeDouble(stream, raw, out.offsetLateral) &&
node::io::takeDouble(stream, raw, out.offsetAlong) &&
node::io::takeDouble(stream, raw, out.offsetVertical) &&
node::io::takeDouble(stream, raw, out.bedLowering);
}
[[nodiscard]] inline bool takeCoreParams(
TokenStream& stream,
std::vector<SourceToken>& raw,
CoreParams& out) {
std::vector<double> numerics;
while (!stream.empty() && !isEnvironment(stream.peek().value)) {
if (!node::io::parseDouble(stream.peek().value)) {
break;
}
double value = 0.0;
if (!node::io::takeDouble(stream, raw, value)) {
return false;
}
numerics.push_back(value);
}
if (numerics.size() >= 1) {
out.length = numerics[0];
}
if (numerics.size() >= 2) {
out.width = numerics[1];
}
if (numerics.size() >= 3) {
out.friction = numerics[2];
}
if (numerics.size() >= 4) {
out.soundDist = numerics[3];
}
if (numerics.size() >= 5) {
out.quality = static_cast<int>(numerics[4]);
}
if (numerics.size() >= 6) {
out.damageFlag = static_cast<int>(numerics[5]);
}
if (!node::io::takeString(stream, raw, out.environment)) {
return false;
}
std::string visToken;
if (!node::io::takeString(stream, raw, visToken)) {
return false;
}
if (isKeywordIgnoreCase(visToken, "unvis") || isKeywordIgnoreCase(visToken, "novis")) {
out.visibilityMode = TrackVisibilityMode::Hidden;
return true;
}
if (isKeywordIgnoreCase(visToken, "vis")) {
out.visibilityMode = TrackVisibilityMode::Visible;
return true;
}
return false;
}
[[nodiscard]] inline bool takeNormalVisibility(
TokenStream& stream,
std::vector<SourceToken>& raw,
NormalRailVisibility& vis) {
return node::io::takeString(stream, raw, vis.railTexture) &&
node::io::takeDouble(stream, raw, vis.railTexLength) &&
node::io::takeString(stream, raw, vis.ballastTexture) &&
node::io::takeDouble(stream, raw, vis.ballastHeight) &&
node::io::takeDouble(stream, raw, vis.ballastWidth) &&
node::io::takeDouble(stream, raw, vis.ballastSlope);
}
[[nodiscard]] inline bool takeSwitchVisibility(
TokenStream& stream,
std::vector<SourceToken>& raw,
SwitchRailVisibility& vis) {
return node::io::takeString(stream, raw, vis.mainRailTexture) &&
node::io::takeDouble(stream, raw, vis.mainRailTexLength) &&
node::io::takeString(stream, raw, vis.switchRailTexture) &&
node::io::takeDouble(stream, raw, vis.unusedHeight) &&
node::io::takeDouble(stream, raw, vis.unusedWidth) &&
node::io::takeDouble(stream, raw, vis.unusedSlope);
}
[[nodiscard]] inline bool takeRoadVisibility(
TokenStream& stream,
std::vector<SourceToken>& raw,
RoadSurfaceVisibility& vis) {
return node::io::takeString(stream, raw, vis.surfaceTexture) &&
node::io::takeDouble(stream, raw, vis.surfaceTexLength) &&
node::io::takeString(stream, raw, vis.shoulderTexture) &&
node::io::takeDouble(stream, raw, vis.shoulderHeight) &&
node::io::takeDouble(stream, raw, vis.shoulderWidth) &&
node::io::takeDouble(stream, raw, vis.shoulderSlope);
}
[[nodiscard]] inline bool takeRiverVisibility(
TokenStream& stream,
std::vector<SourceToken>& raw,
RiverVisibility& vis) {
return node::io::takeString(stream, raw, vis.waterTexture) &&
node::io::takeDouble(stream, raw, vis.waterTexLength) &&
node::io::takeString(stream, raw, vis.bankTexture) &&
node::io::takeDouble(stream, raw, vis.bankHeight) &&
node::io::takeDouble(stream, raw, vis.leftBankWidth) &&
node::io::takeDouble(stream, raw, vis.rightBankWidth);
}
[[nodiscard]] inline bool parseOptionalKeyword(
TokenStream& stream,
std::vector<SourceToken>& raw,
const std::string_view key,
TrackOptionals& out) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
if (isKeyword(key, "velocity")) {
double vel = 0.0;
if (!node::io::takeDouble(stream, raw, vel)) {
return false;
}
out.velocity = vel;
return true;
}
if (isKeyword(key, "isolated")) {
std::string name;
if (!node::io::takeString(stream, raw, name)) {
return false;
}
out.isolated.push_back(std::move(name));
return true;
}
if (isKeyword(key, "overhead")) {
double value = 0.0;
if (!node::io::takeDouble(stream, raw, value)) {
return false;
}
out.overhead = value;
return true;
}
if (isKeyword(key, "railprofile")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.railProfile = std::move(value);
return true;
}
if (isKeyword(key, "trackbed")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.trackBed = std::move(value);
return true;
}
if (isKeyword(key, "friction")) {
double value = 0.0;
if (!node::io::takeDouble(stream, raw, value)) {
return false;
}
out.frictionOverride = value;
return true;
}
if (isKeyword(key, "vradius")) {
double value = 0.0;
if (!node::io::takeDouble(stream, raw, value)) {
return false;
}
out.verticalRadius = value;
return true;
}
if (isKeyword(key, "angle1")) {
double value = 0.0;
if (!node::io::takeDouble(stream, raw, value)) {
return false;
}
out.angle1 = value;
return true;
}
if (isKeyword(key, "angle2")) {
double value = 0.0;
if (!node::io::takeDouble(stream, raw, value)) {
return false;
}
out.angle2 = value;
return true;
}
if (isKeyword(key, "fouling1")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.fouling1 = std::move(value);
return true;
}
if (isKeyword(key, "fouling2")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.fouling2 = std::move(value);
return true;
}
if (isKeyword(key, "sleepermodel")) {
TrackSleeperModel sleeper;
if (!takeSleeperModel(stream, raw, sleeper)) {
return false;
}
out.sleeperModel = std::move(sleeper);
return true;
}
if (isKeyword(key, "event0")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.events.occupiedStop = std::move(value);
return true;
}
if (isKeyword(key, "event1")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.events.occupiedEnterP1 = std::move(value);
return true;
}
if (isKeyword(key, "event2")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.events.occupiedEnterP2 = std::move(value);
return true;
}
if (isKeyword(key, "eventall0")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.events.anyStop = std::move(value);
return true;
}
if (isKeyword(key, "eventall1")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.events.anyEnterP1 = std::move(value);
return true;
}
if (isKeyword(key, "eventall2")) {
std::string value;
if (!node::io::takeString(stream, raw, value)) {
return false;
}
out.events.anyEnterP2 = std::move(value);
return true;
}
std::string arg;
if (!node::io::takeString(stream, raw, arg)) {
return false;
}
out.extraKeywords.emplace_back(std::string(key), std::move(arg));
return true;
}
[[nodiscard]] inline bool parseTail(
TokenStream& stream,
std::vector<SourceToken>& raw,
std::vector<TrackBezier>& beziers,
TrackOptionals& optionals) {
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
const std::string_view value = stream.peek().value;
if (isTrackKeyword(value)) {
if (!parseOptionalKeyword(stream, raw, value, optionals)) {
return false;
}
continue;
}
if (!node::io::parseDouble(stream.peek().value)) {
break;
}
TrackBezier segment;
if (!takeBezier(stream, raw, segment)) {
return false;
}
beziers.push_back(segment);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
[[nodiscard]] inline bool parseNormalBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
const std::string& trajectoryKind,
ParsedTrackNormal& out) {
out.header = header;
out.trajectoryKind = trajectoryKind;
CoreParams core;
if (!takeCoreParams(stream, raw, core)) {
return false;
}
out.length = core.length;
out.gauge = core.width;
out.friction = core.friction;
out.soundDist = core.soundDist;
out.quality = core.quality;
out.damageFlag = core.damageFlag;
out.environment = std::move(core.environment);
out.visibilityMode = core.visibilityMode;
if (out.visibilityMode == TrackVisibilityMode::Visible) {
NormalRailVisibility vis;
if (!takeNormalVisibility(stream, raw, vis)) {
return false;
}
out.visibility = std::move(vis);
}
return parseTail(stream, raw, out.beziers, out.optionals);
}
[[nodiscard]] inline bool parseSwitchBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedTrackSwitch& out) {
out.header = header;
CoreParams core;
if (!takeCoreParams(stream, raw, core)) {
return false;
}
out.length = core.length;
out.gauge = core.width;
out.friction = core.friction;
out.soundDist = core.soundDist;
out.quality = core.quality;
out.damageFlag = core.damageFlag;
out.environment = std::move(core.environment);
out.visibilityMode = core.visibilityMode;
if (out.visibilityMode == TrackVisibilityMode::Visible) {
SwitchRailVisibility vis;
if (!takeSwitchVisibility(stream, raw, vis)) {
return false;
}
out.visibility = std::move(vis);
}
return parseTail(stream, raw, out.beziers, out.optionals);
}
[[nodiscard]] inline bool parseRoadBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedTrackRoad& out) {
out.header = header;
CoreParams core;
if (!takeCoreParams(stream, raw, core)) {
return false;
}
out.length = core.length;
out.roadWidth = core.width;
out.friction = core.friction;
out.soundDist = core.soundDist;
out.quality = core.quality;
out.damageFlag = core.damageFlag;
out.environment = std::move(core.environment);
out.visibilityMode = core.visibilityMode;
if (out.visibilityMode == TrackVisibilityMode::Visible) {
RoadSurfaceVisibility vis;
if (!takeRoadVisibility(stream, raw, vis)) {
return false;
}
out.visibility = std::move(vis);
}
return parseTail(stream, raw, out.beziers, out.optionals);
}
[[nodiscard]] inline bool parseCrossBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedTrackCross& out) {
out.header = header;
CoreParams core;
if (!takeCoreParams(stream, raw, core)) {
return false;
}
out.length = core.length;
out.roadWidth = core.width;
out.friction = core.friction;
out.soundDist = core.soundDist;
out.quality = core.quality;
out.damageFlag = core.damageFlag;
out.environment = std::move(core.environment);
out.visibilityMode = core.visibilityMode;
if (out.visibilityMode == TrackVisibilityMode::Visible) {
RoadSurfaceVisibility vis;
if (!takeRoadVisibility(stream, raw, vis)) {
return false;
}
out.visibility = std::move(vis);
}
return parseTail(stream, raw, out.beziers, out.optionals);
}
[[nodiscard]] inline bool parseOtherBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
const std::string& trajectoryKind,
ParsedTrackOther& out,
const bool riverLike) {
out.header = header;
out.trajectoryKind = trajectoryKind;
CoreParams core;
if (!takeCoreParams(stream, raw, core)) {
return false;
}
out.length = core.length;
out.width = core.width;
out.friction = core.friction;
out.soundDist = core.soundDist;
out.quality = core.quality;
out.damageFlag = core.damageFlag;
out.environment = std::move(core.environment);
out.visibilityMode = core.visibilityMode;
if (out.visibilityMode == TrackVisibilityMode::Visible) {
if (riverLike) {
RiverVisibility vis;
if (!takeRiverVisibility(stream, raw, vis)) {
return false;
}
out.riverVisibility = std::move(vis);
} else {
NormalRailVisibility vis;
if (!takeNormalVisibility(stream, raw, vis)) {
return false;
}
out.railVisibility = std::move(vis);
}
}
return parseTail(stream, raw, out.beziers, out.optionals);
}
[[nodiscard]] inline bool isNormalLike(const std::string_view kind) noexcept {
return isKeyword(kind, "normal") || isKeyword(kind, "turn") || isKeyword(kind, "table");
}
[[nodiscard]] inline bool isRiverLike(const std::string_view kind) noexcept {
return isKeyword(kind, "river") || isKeyword(kind, "tributary");
}
} // namespace node_track
} // namespace eu07::scene

View File

@@ -0,0 +1,100 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node::traction
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/shared.hpp>
#include <eu07/scene/node/types.hpp>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeTraction {
NodeHeader header;
std::string powerSourceName;
double nominalVoltage = 0.0;
double maxCurrent = 0.0;
double resistivity = 0.0;
TractionMaterialKind materialKind = TractionMaterialKind::Copper;
std::string materialRaw;
double wireThickness = 0.0;
int damageFlag = 0;
Vec3 wireLowerStart;
Vec3 wireLowerEnd;
Vec3 wireUpperStart;
Vec3 wireUpperEnd;
double minHeight = 0.0;
double segmentLength = 0.0;
int wires = 0;
double wireOffset = 0.0;
NodeVisibilityMode visibilityMode = NodeVisibilityMode::Hidden;
std::optional<std::string> parallel;
std::vector<SourceToken> raw;
};
namespace node_traction {
inline constexpr std::string_view kSubtype = "traction";
inline constexpr std::string_view kEndMarker = "endtraction";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeTraction& out) {
out.header = header;
out.raw.clear();
double damageFlag = 0.0;
double wires = 0.0;
if (!node::io::takeString(stream, raw, out.powerSourceName) ||
!node::io::takeDouble(stream, raw, out.nominalVoltage) ||
!node::io::takeDouble(stream, raw, out.maxCurrent) ||
!node::io::takeDouble(stream, raw, out.resistivity) ||
!node::io::takeString(stream, raw, out.materialRaw) ||
!node::io::takeDouble(stream, raw, out.wireThickness) ||
!node::io::takeDouble(stream, raw, damageFlag) ||
!node::io::takeVec3(stream, raw, out.wireLowerStart) ||
!node::io::takeVec3(stream, raw, out.wireLowerEnd) ||
!node::io::takeVec3(stream, raw, out.wireUpperStart) ||
!node::io::takeVec3(stream, raw, out.wireUpperEnd) ||
!node::io::takeDouble(stream, raw, out.minHeight) ||
!node::io::takeDouble(stream, raw, out.segmentLength) ||
!node::io::takeDouble(stream, raw, wires) ||
!node::io::takeDouble(stream, raw, out.wireOffset)) {
return false;
}
out.damageFlag = static_cast<int>(damageFlag);
out.wires = static_cast<int>(wires);
out.materialKind = parseTractionMaterial(out.materialRaw);
std::string vis;
if (!node::io::takeString(stream, raw, vis)) {
return false;
}
out.visibilityMode = parseNodeVisibility(vis);
if (!stream.empty() && isKeyword(stream.peek().value, "parallel")) {
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
std::string name;
if (!node::io::takeString(stream, raw, name)) {
return false;
}
out.parallel = std::move(name);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_traction
} // namespace eu07::scene

View File

@@ -0,0 +1,80 @@
#pragma once
// https://wiki.eu07.pl — node tractionpowersource (world/TractionPower.cpp)
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeTractionPowerSource {
NodeHeader header;
Vec3 position;
double nominalVoltage = 0.0;
double voltageFrequency = 0.0;
double internalResistance = 0.2;
double maxOutputCurrent = 0.0;
double fastFuseTimeout = 0.0;
double fastFuseRepetition = 0.0;
double slowFuseTimeout = 0.0;
bool recuperation = false;
bool section = false;
std::vector<SourceToken> raw;
};
namespace node_traction_power {
inline constexpr std::string_view kSubtype = "tractionpowersource";
inline constexpr std::string_view kEndMarker = "end";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeTractionPowerSource& out) {
out.header = header;
out.raw.clear();
out.recuperation = false;
out.section = false;
if (!node::io::takeVec3(stream, raw, out.position) ||
!node::io::takeDouble(stream, raw, out.nominalVoltage) ||
!node::io::takeDouble(stream, raw, out.voltageFrequency) ||
!node::io::takeDouble(stream, raw, out.internalResistance) ||
!node::io::takeDouble(stream, raw, out.maxOutputCurrent) ||
!node::io::takeDouble(stream, raw, out.fastFuseTimeout) ||
!node::io::takeDouble(stream, raw, out.fastFuseRepetition) ||
!node::io::takeDouble(stream, raw, out.slowFuseTimeout)) {
return false;
}
while (!stream.empty()) {
std::string token;
if (!node::io::takeString(stream, raw, token)) {
return false;
}
if (isKeyword(token, kEndMarker)) {
return true;
}
if (isKeywordIgnoreCase(token, "recuperation")) {
out.recuperation = true;
continue;
}
if (isKeywordIgnoreCase(token, "section")) {
out.section = true;
continue;
}
}
return false;
}
} // namespace node_traction_power
} // namespace eu07::scene

View File

@@ -0,0 +1,36 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/mesh.hpp>
#include <eu07/scene/node/types.hpp>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeTriangleFan {
NodeHeader header;
std::string texture;
std::vector<MeshVertex> vertices;
std::vector<SourceToken> raw;
};
namespace node_triangle_fan {
inline constexpr std::string_view kSubtype = "triangle_fan";
inline constexpr std::string_view kEndMarker = "endtriangle_fan";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeTriangleFan& out) {
return node::io::parseTexturedMeshBody(stream, raw, header, kSubtype, kEndMarker, out);
}
} // namespace node_triangle_fan
} // namespace eu07::scene

View File

@@ -0,0 +1,36 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/mesh.hpp>
#include <eu07/scene/node/types.hpp>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeTriangleStrip {
NodeHeader header;
std::string texture;
std::vector<MeshVertex> vertices;
std::vector<SourceToken> raw;
};
namespace node_triangle_strip {
inline constexpr std::string_view kSubtype = "triangle_strip";
inline constexpr std::string_view kEndMarker = "endtriangle_strip";
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeTriangleStrip& out) {
return node::io::parseTexturedMeshBody(stream, raw, header, kSubtype, kEndMarker, out);
}
} // namespace node_triangle_strip
} // namespace eu07::scene

View File

@@ -0,0 +1,122 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Obiekt_node::triangles
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
#include <string>
#include <vector>
namespace eu07::scene {
struct ParsedNodeTriangles {
NodeHeader header;
NodeMaterial material;
std::string texture;
std::vector<MeshVertex> vertices;
std::vector<SourceToken> raw;
};
namespace node_triangles {
inline constexpr std::string_view kSubtype = "triangles";
inline constexpr std::string_view kEndMarker = "endtriangles";
[[nodiscard]] inline bool parseMaterial(
TokenStream& stream,
std::vector<SourceToken>& raw,
NodeMaterial& material) {
if (stream.empty() || !isKeyword(stream.peek().value, "material")) {
return false;
}
SourceToken kw;
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
auto matchesMaterialLabel = [](const std::string_view token, const std::string_view label) noexcept {
if (isKeyword(token, label)) {
return true;
}
// MaSzyna tokenizuje "ambient:" jako jeden token (scenenode.cpp).
return token.size() == label.size() + 1 && token.back() == ':' &&
isKeyword(token.substr(0, label.size()), label);
};
auto takeComponent = [&](const std::string_view label, MaterialRgb& rgb) -> bool {
if (stream.empty() || !matchesMaterialLabel(stream.peek().value, label)) {
return false;
}
if (!node::io::takeToken(stream, raw, kw)) {
return false;
}
return node::io::takeDouble(stream, raw, rgb.r) && node::io::takeDouble(stream, raw, rgb.g) &&
node::io::takeDouble(stream, raw, rgb.b);
};
if (!takeComponent("ambient", material.ambient) || !takeComponent("diffuse", material.diffuse) ||
!takeComponent("specular", material.specular)) {
return false;
}
if (stream.empty() || !isKeyword(stream.peek().value, "endmaterial")) {
return false;
}
return node::io::takeToken(stream, raw, kw);
}
[[nodiscard]] inline bool takeVertex(
TokenStream& stream,
std::vector<SourceToken>& raw,
MeshVertex& vertex,
const bool allowEndSuffix) {
if (!node::io::takeDouble(stream, raw, vertex.x) || !node::io::takeDouble(stream, raw, vertex.y) ||
!node::io::takeDouble(stream, raw, vertex.z) || !node::io::takeDouble(stream, raw, vertex.nx) ||
!node::io::takeDouble(stream, raw, vertex.ny) || !node::io::takeDouble(stream, raw, vertex.nz) ||
!node::io::takeDouble(stream, raw, vertex.u) || !node::io::takeDouble(stream, raw, vertex.v)) {
return false;
}
if (allowEndSuffix && !stream.empty() && isKeyword(stream.peek().value, "end")) {
SourceToken endToken;
if (!node::io::takeToken(stream, raw, endToken)) {
return false;
}
}
return true;
}
[[nodiscard]] inline bool parseBody(
TokenStream& stream,
std::vector<SourceToken>& raw,
const NodeHeader& header,
ParsedNodeTriangles& out) {
out.header = header;
out.raw.clear();
if (!stream.empty() && isKeyword(stream.peek().value, "material")) {
if (!parseMaterial(stream, raw, out.material) ||
!node::io::takeString(stream, raw, out.texture)) {
return false;
}
} else if (!node::io::takeString(stream, raw, out.texture)) {
return false;
}
while (!stream.empty() && !node::io::atEnd(stream, kSubtype, kEndMarker)) {
MeshVertex vertex;
const bool hasMore = stream.remaining() > 1;
if (!takeVertex(stream, raw, vertex, hasMore)) {
return false;
}
out.vertices.push_back(vertex);
}
return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker);
}
} // namespace node_triangles
} // namespace eu07::scene

View File

@@ -0,0 +1,38 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene {
struct Vec3 {
double x = 0.0;
double y = 0.0;
double z = 0.0;
};
struct MeshVertex {
double x = 0.0;
double y = 0.0;
double z = 0.0;
double nx = 0.0;
double ny = 0.0;
double nz = 0.0;
double u = 0.0;
double v = 0.0;
};
struct MaterialRgb {
double r = 0.0;
double g = 0.0;
double b = 0.0;
};
struct NodeMaterial {
MaterialRgb ambient;
MaterialRgb diffuse;
MaterialRgb specular;
};
} // namespace eu07::scene

View File

@@ -0,0 +1,40 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_origin
// origin x y z … endorigin — push offsetu na stos, reszta idzie główną pętlą.
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
namespace eu07::scene::origin {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty()) {
return false;
}
const std::string_view kw = stream.peek().value;
if (!isKeyword(kw, "origin") && !isKeyword(kw, "orgin")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
Vec3 offset;
std::vector<SourceToken> raw;
if (!node::io::takeDoubleOrPlaceholder(stream, raw, offset.x) ||
!node::io::takeDoubleOrPlaceholder(stream, raw, offset.y) ||
!node::io::takeDoubleOrPlaceholder(stream, raw, offset.z)) {
return false;
}
block.tokens = std::move(raw);
ctx.scratch.pushOrigin(offset);
ctx.document.origin.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::origin

View File

@@ -0,0 +1,243 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Plik_scenerii
#include <eu07/parser.hpp>
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/dispatch_table.hpp>
#include <eu07/scene/document.hpp>
#include <eu07/scene/include_resolve.hpp>
#include <filesystem>
#include <system_error>
namespace eu07::scene {
namespace detail {
inline ParsedInclude* includeEntryAt(ParseContext& context, const std::size_t entryIndex) noexcept {
if (entryIndex >= context.document.include.size()) {
return nullptr;
}
return &context.document.include[entryIndex];
}
inline void processStream(TokenStream& stream, ParseContext& context) {
while (!stream.empty()) {
if (dispatchDirective(stream, context)) {
continue;
}
UnknownEntry unknown;
unknown.line = stream.peek().sourceLine;
unknown.token = stream.consume().value;
context.document.unknown.push_back(std::move(unknown));
}
}
inline void expandInclude(ParseContext& context, const IncludeExpansionRequest& request) {
ParsedInclude* const entry = includeEntryAt(context, request.entryIndex);
if (entry == nullptr) {
return;
}
if (request.file.empty()) {
entry->error = "pusta sciezka include";
return;
}
const std::filesystem::path resolved = detail::resolveExpandedIncludePath(
context.baseDirectory, context.includeStack, request.file);
if (detail::isIncludeCycle(resolved, context.includeStack)) {
entry->error = "cykl include: " + resolved.string();
return;
}
std::error_code existsEc;
if (!std::filesystem::exists(resolved, existsEc)) {
entry->error = "nie znaleziono pliku: " + resolved.string();
return;
}
context.includeStack.push_back(resolved.lexically_normal());
const bool wrapIncGroup = detail::isIncFile(request.file);
if (wrapIncGroup) {
context.scratch.openGroup();
}
context.activeIncludeParameters.push_back(request.parameters);
try {
const eu07::ParseResult included = detail::parseIncludedFile(resolved, request.parameters);
TokenStream childStream(included.tokens);
processStream(childStream, context);
if (ParsedInclude* const done = includeEntryAt(context, request.entryIndex)) {
done->expanded = true;
}
} catch (const std::exception& ex) {
if (ParsedInclude* const failed = includeEntryAt(context, request.entryIndex)) {
failed->error = ex.what();
}
}
context.activeIncludeParameters.pop_back();
if (wrapIncGroup) {
context.scratch.closeGroup();
}
if (!context.includeStack.empty()) {
context.includeStack.pop_back();
}
}
} // namespace detail
struct SceneProcessOptions {
bool expandIncludes = true;
};
[[nodiscard]] inline SceneProcessResult processScene(
const ParseResult& parsed,
const std::filesystem::path& baseDirectory = {},
const SceneProcessOptions& options = {}) {
TokenStream stream(parsed.tokens);
SceneDocument document;
std::filesystem::path includeRoot = baseDirectory;
if (includeRoot.empty()) {
includeRoot = std::filesystem::current_path();
}
ParseContext context{document, {}, includeRoot, {}};
context.expandIncludes = options.expandIncludes;
detail::processStream(stream, context);
SceneProcessResult result;
result.document = std::move(document);
return result;
}
} // namespace eu07::scene

View File

@@ -0,0 +1,202 @@
#pragma once
#include <eu07/scene/document.hpp>
#include <eu07/scene/node/header.hpp>
#include <eu07/scene/node/types.hpp>
#include <filesystem>
#include <fstream>
#include <ostream>
#include <stdexcept>
#include <string_view>
namespace eu07::scene {
namespace detail {
inline void writeHeader(std::ostream& out, const NodeHeader& header) {
out << "L" << (header.line + 1) << " range_max=" << header.rangeMax
<< " range_min=" << header.rangeMin << " name=" << header.name;
}
inline void writeVec3(std::ostream& out, const Vec3& v) {
out << v.x << ' ' << v.y << ' ' << v.z;
}
template <typename Node>
inline void writeNodeList(std::ostream& out, const std::string_view section, const std::vector<Node>& nodes) {
out << "\n[" << section << "] count=" << nodes.size() << '\n';
}
} // namespace detail
inline void writeSceneReport(const std::filesystem::path& outPath, const SceneDocument& doc) {
std::ofstream out(outPath);
if (!out) {
throw std::runtime_error("Nie mozna zapisac: " + outPath.string());
}
out << "# raport scenariusza — sparsowane struktury node + tokeny pozostalych dyrektyw\n\n";
out << "[podsumowanie] node=" << countNodeInstances(doc) << " razem=" << countDirectiveInstances(doc)
<< " nieznane=" << doc.unknown.size() << " include=" << doc.include.size() << '\n';
detail::writeNodeList(out, "include", doc.include);
for (const ParsedInclude& inc : doc.include) {
out << "L" << (inc.line + 1) << " file=" << inc.file;
if (!inc.parameters.empty()) {
out << " params=" << inc.parameters.size();
}
out << " expanded=" << (inc.expanded ? "yes" : "no");
if (inc.error) {
out << " error=" << *inc.error;
}
out << '\n';
}
detail::writeNodeList(out, "node_dynamic", doc.nodeDynamic);
for (const ParsedNodeDynamic& node : doc.nodeDynamic) {
detail::writeHeader(out, node.header);
out << " folder=" << node.datafolder << " skin=" << node.skinfile << " mmd=" << node.mmdfile
<< " track=" << node.pathname << " driver=" << node.driverType
<< " coupling=" << node.coupling.raw;
if (node.inTrainset) {
out << " trainset";
}
if (node.destination) {
out << " dest=" << *node.destination;
}
out << '\n';
}
detail::writeNodeList(out, "node_model", doc.nodeModel);
for (const ParsedNodeModel& node : doc.nodeModel) {
detail::writeHeader(out, node.header);
out << " pos=";
detail::writeVec3(out, node.location);
out << " rotY=" << node.rotationY << " model=" << node.modelPath << '\n';
}
detail::writeNodeList(out, "node_track_normal", doc.nodeTrackNormal);
for (const ParsedTrackNormal& node : doc.nodeTrackNormal) {
detail::writeHeader(out, node.header);
out << " kind=" << node.trajectoryKind << " gauge=" << node.gauge << " env=" << node.environment
<< " beziers=" << node.beziers.size() << '\n';
}
detail::writeNodeList(out, "node_track_switch", doc.nodeTrackSwitch);
for (const ParsedTrackSwitch& node : doc.nodeTrackSwitch) {
detail::writeHeader(out, node.header);
out << " gauge=" << node.gauge << " env=" << node.environment
<< " beziers=" << node.beziers.size() << '\n';
}
detail::writeNodeList(out, "node_track_road", doc.nodeTrackRoad);
for (const ParsedTrackRoad& node : doc.nodeTrackRoad) {
detail::writeHeader(out, node.header);
out << " width=" << node.roadWidth << " env=" << node.environment
<< " beziers=" << node.beziers.size() << '\n';
}
detail::writeNodeList(out, "node_track_cross", doc.nodeTrackCross);
for (const ParsedTrackCross& node : doc.nodeTrackCross) {
detail::writeHeader(out, node.header);
out << " width=" << node.roadWidth << " env=" << node.environment
<< " beziers=" << node.beziers.size() << '\n';
}
detail::writeNodeList(out, "node_track_other", doc.nodeTrackOther);
for (const ParsedTrackOther& node : doc.nodeTrackOther) {
detail::writeHeader(out, node.header);
out << " kind=" << node.trajectoryKind << " width=" << node.width << " env=" << node.environment
<< " beziers=" << node.beziers.size() << '\n';
}
detail::writeNodeList(out, "node_traction", doc.nodeTraction);
for (const ParsedNodeTraction& node : doc.nodeTraction) {
detail::writeHeader(out, node.header);
out << " supply=" << node.powerSourceName << " U=" << node.nominalVoltage << '\n';
}
detail::writeNodeList(out, "node_traction_power", doc.nodeTractionPower);
for (const ParsedNodeTractionPowerSource& node : doc.nodeTractionPower) {
detail::writeHeader(out, node.header);
out << " U=" << node.nominalVoltage << " Imax=" << node.maxOutputCurrent;
if (node.recuperation) {
out << " recuperation";
}
if (node.section) {
out << " section";
}
out << '\n';
}
detail::writeNodeList(out, "node_triangles", doc.nodeTriangles);
for (const ParsedNodeTriangles& node : doc.nodeTriangles) {
detail::writeHeader(out, node.header);
out << " tex=" << node.texture << " vertices=" << node.vertices.size() << '\n';
}
detail::writeNodeList(out, "node_triangle_strip", doc.nodeTriangleStrip);
for (const ParsedNodeTriangleStrip& node : doc.nodeTriangleStrip) {
detail::writeHeader(out, node.header);
out << " tex=" << node.texture << " vertices=" << node.vertices.size() << '\n';
}
detail::writeNodeList(out, "node_triangle_fan", doc.nodeTriangleFan);
for (const ParsedNodeTriangleFan& node : doc.nodeTriangleFan) {
detail::writeHeader(out, node.header);
out << " tex=" << node.texture << " vertices=" << node.vertices.size() << '\n';
}
detail::writeNodeList(out, "node_lines", doc.nodeLines);
for (const ParsedNodeLines& node : doc.nodeLines) {
detail::writeHeader(out, node.header);
out << " segments=" << node.segments.size() << '\n';
}
detail::writeNodeList(out, "node_line_strip", doc.nodeLineStrip);
for (const ParsedNodeLineStrip& node : doc.nodeLineStrip) {
detail::writeHeader(out, node.header);
out << " points=" << node.points.size() << '\n';
}
detail::writeNodeList(out, "node_line_loop", doc.nodeLineLoop);
for (const ParsedNodeLineLoop& node : doc.nodeLineLoop) {
detail::writeHeader(out, node.header);
out << " points=" << node.points.size() << '\n';
}
detail::writeNodeList(out, "node_memcell", doc.nodeMemcell);
for (const ParsedNodeMemcell& node : doc.nodeMemcell) {
detail::writeHeader(out, node.header);
out << " cmd=" << node.command;
if (node.trackName) {
out << " track=" << *node.trackName;
}
out << '\n';
}
detail::writeNodeList(out, "node_eventlauncher", doc.nodeEventlauncher);
for (const ParsedNodeEventlauncher& node : doc.nodeEventlauncher) {
detail::writeHeader(out, node.header);
out << " key=" << node.key << " event1=" << node.event1;
if (node.event2) {
out << " event2=" << *node.event2;
}
if (node.condition) {
out << " condition";
}
if (node.trainTriggered) {
out << " traintriggered";
}
out << '\n';
}
detail::writeNodeList(out, "node_sound", doc.nodeSound);
for (const ParsedNodeSound& node : doc.nodeSound) {
detail::writeHeader(out, node.header);
out << " wav=" << node.wavFile << '\n';
}
}
} // namespace eu07::scene

View File

@@ -0,0 +1,36 @@
#pragma once
// https://wiki.eu07.pl/index.php?title=Dyrektywa_rotate
// rotate x y z — nadpisuje rotację, bez endrotate i bez połykania kolejnych tokenów.
#include <eu07/scene/context.hpp>
#include <eu07/scene/cursor.hpp>
#include <eu07/scene/match.hpp>
#include <eu07/scene/node/parse_util.hpp>
#include <eu07/scene/node/types.hpp>
namespace eu07::scene::rotate {
[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) {
if (stream.empty() || !isKeyword(stream.peek().value, "rotate")) {
return false;
}
DirectiveBlock block;
block.line = stream.consume().sourceLine;
Vec3 rotation;
std::vector<SourceToken> raw;
if (!node::io::takeDoubleOrPlaceholder(stream, raw, rotation.x) ||
!node::io::takeDoubleOrPlaceholder(stream, raw, rotation.y) ||
!node::io::takeDoubleOrPlaceholder(stream, raw, rotation.z)) {
return false;
}
block.tokens = std::move(raw);
ctx.scratch.setRotation(rotation);
ctx.document.rotate.push_back(std::move(block));
return true;
}
} // namespace eu07::scene::rotate

View File

@@ -0,0 +1,23 @@
#pragma once
// Kontrakt runtime MaSzyny — structy docelowe po state_serializer.
//
// Zrodla w maszyna-fresh:
// simulation/simulationstateserializer.cpp (deserialize_*, transform)
// scene/scenenode.h / scenenode.cpp (shape_node, lines_node, basic_node)
// model/AnimModel.cpp (TAnimModel::Load)
// world/Track.cpp, Segment.cpp (TTrack::Load, segment_data)
// world/Traction.cpp, TractionPower.cpp
// world/MemCell.cpp, EvLaunch.cpp
// world/Event.cpp (make_event, basic_event)
//
// Warstwa Parsed* (document.hpp) = AST z tekstu.
// Warstwa runtime::* = pola, ktore symulator trzyma po imporcie.
#include <eu07/scene/runtime/basic_node.hpp>
#include <eu07/scene/runtime/directives.hpp>
#include <eu07/scene/runtime/nodes.hpp>
#include <eu07/scene/runtime/scene.hpp>
#include <eu07/scene/runtime/scratch.hpp>
#include <eu07/scene/runtime/transform.hpp>
#include <eu07/scene/runtime/types.hpp>

View File

@@ -0,0 +1,45 @@
#pragma once
// scenenode.h — basic_node (wspolna baza TTrack, TAnimModel, TMemCell, …)
#include <eu07/scene/runtime/types.hpp>
#include <cstddef>
#include <string>
#include <vector>
namespace eu07::scene::runtime {
// Snapshot stosu origin/scale/rotate z momentu parsowania node'a (do detokenizacji).
struct TransformContext {
std::vector<Vec3> originStack;
std::vector<Vec3> scaleStack;
Vec3 rotation{};
std::size_t groupStackDepth = 0;
};
struct BasicNode {
BoundingArea area;
double rangeSquaredMin = 0.0;
double rangeSquaredMax = 0.0;
bool visible = true;
std::string name;
std::string nodeType;
std::size_t groupHandle = 0;
bool groupValid = false;
TransformContext transform;
std::string uuid;
bool dirty = false;
};
[[nodiscard]] inline BasicNode makeBasicNode(const NodeData& data) {
BasicNode node;
node.name = (data.name == "none") ? std::string{} : data.name;
node.nodeType = data.type;
node.rangeSquaredMin = data.rangeMin * data.rangeMin;
node.rangeSquaredMax =
data.rangeMax >= 0.0 ? data.rangeMax * data.rangeMax : std::numeric_limits<double>::max();
return node;
}
} // namespace eu07::scene::runtime

View File

@@ -0,0 +1,77 @@
#pragma once
// Dyrektywy scenariusza poza `node`: trainset, event, transform stack.
#include <eu07/scene/runtime/nodes.hpp>
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
namespace eu07::scene::runtime {
// trainset / endtrainset
struct RuntimeTrainset {
std::string name;
std::string track;
float offset = 0.f;
float velocity = 0.f;
std::unordered_map<std::string, std::string> assignment;
std::vector<std::size_t> vehicleIndices;
std::vector<int> couplings;
std::size_t driverIndex = static_cast<std::size_t>(-1);
};
// event / endevent — world/Event.h basic_event + podklasy
enum class EventType : std::uint8_t {
AddValues,
UpdateValues,
CopyValues,
GetValues,
PutValues,
Whois,
LogValues,
Multiple,
Switch,
TrackVel,
Sound,
Texture,
Animation,
Lights,
Voltage,
Visible,
Friction,
Message,
Unknown,
};
struct RuntimeEvent {
std::string name;
EventType type = EventType::Unknown;
double delay = 0.0;
std::vector<std::string> targets;
double delayRandom = 0.0;
double delayDeparture = 0.0;
bool ignored = false;
bool passive = false;
// type-specific payload — union lub osobne structy w przyszlosci
std::vector<std::pair<std::string, std::string>> payload;
};
enum class TransformOp : std::uint8_t {
PushOrigin,
PopOrigin,
PushScale,
PopScale,
SetRotation,
GroupBegin,
GroupEnd,
};
struct TransformDirective {
TransformOp op = TransformOp::PushOrigin;
Vec3 vector{};
};
} // namespace eu07::scene::runtime

View File

@@ -0,0 +1,220 @@
#pragma once
// Payload per typ `node` — odpowiedniki klas w maszyna-fresh.
#include <eu07/scene/runtime/basic_node.hpp>
#include <eu07/scene/runtime/types.hpp>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace eu07::scene::runtime {
// --- model → TAnimModel (model/AnimModel.h) ---
struct RuntimeModelInstance {
BasicNode node;
Vec3 location;
Vec3 angles;
Vec3 scale{1.0, 1.0, 1.0};
std::string modelFile;
std::string textureFile;
std::vector<float> lightStates;
std::vector<std::uint32_t> lightColors;
Vec3 inlineScale{1.0, 1.0, 1.0};
bool transition = true;
bool isTerrain = false;
};
// --- triangles / triangle_strip / triangle_fan → shape_node ---
struct RuntimeShapeNode {
BasicNode node;
bool translucent = false;
std::string materialPath;
LightingData lighting;
Vec3 origin;
std::vector<WorldVertex> vertices;
};
// --- lines / line_strip / line_loop → lines_node ---
struct RuntimeLinesNode {
BasicNode node;
LightingData lighting;
float lineWidth = 1.f;
Vec3 origin;
std::vector<WorldVertex> vertices;
};
// --- track → TTrack (world/Track.h) ---
enum class TrackType : std::uint8_t {
Unknown,
Normal,
Switch,
Table,
Cross,
Tributary,
};
enum class TrackCategory : std::uint8_t {
Rail = 1,
Road = 2,
Water = 4,
};
enum class TrackEnvironment : std::int8_t {
Unknown = -1,
Flat = 0,
Mountains,
Canyon,
Tunnel,
Bridge,
Bank,
};
struct TrackVisibility {
std::string material1;
float texLength = 4.f;
std::string material2;
float texHeight1 = 0.f;
float texWidth = 0.f;
float texSlope = 0.f;
};
struct RuntimeTrack {
BasicNode node;
TrackType trackType = TrackType::Unknown;
TrackCategory category = TrackCategory::Rail;
float length = 0.f;
float trackWidth = 0.f;
float friction = 0.f;
float soundDistance = 0.f;
int qualityFlag = 0;
int damageFlag = 0;
TrackEnvironment environment = TrackEnvironment::Unknown;
std::optional<TrackVisibility> visibility;
std::vector<SegmentPath> paths;
// tail TTrack::Load (event0, fouling, velocity, …) — rozszerzalne pozniej
std::vector<std::pair<std::string, std::string>> tailKeywords;
};
// --- traction → TTraction (world/Traction.h) ---
enum class TractionWireMaterial : std::uint8_t {
None = 0,
Copper = 1,
Aluminium = 2,
};
struct RuntimeTraction {
BasicNode node;
std::string powerSupplyName;
float nominalVoltage = 0.f;
float maxCurrent = 0.f;
float resistivityOhmPerM = 0.f;
double resistivityLegacy = 0.0;
std::string materialRaw = "cu";
TractionWireMaterial material = TractionWireMaterial::Copper;
float wireThickness = 0.f;
int damageFlag = 0;
Vec3 wireP1;
Vec3 wireP2;
Vec3 wireP3;
Vec3 wireP4;
double minHeight = 0.0;
double segmentLength = 0.0;
int wireCount = 0;
float wireOffset = 0.f;
std::optional<std::string> parallelName;
};
// --- tractionpowersource → TTractionPowerSource (world/TractionPower.cpp) ---
enum class PowerSourceModifier : std::uint8_t {
None,
Recuperation,
Section,
};
struct RuntimeTractionPowerSource {
BasicNode node;
Vec3 position{};
float nominalVoltage = 0.f;
float voltageFrequency = 0.f;
double internalResistanceLegacy = 0.2;
float internalResistance = 0.2f;
float maxOutputCurrent = 0.f;
float fastFuseTimeout = 0.f;
float fastFuseRepetition = 0.f;
float slowFuseTimeout = 0.f;
PowerSourceModifier modifier = PowerSourceModifier::None;
};
// --- memcell → TMemCell (world/MemCell.h) ---
struct RuntimeMemCell {
BasicNode node;
std::string text;
double value1 = 0.0;
double value2 = 0.0;
std::optional<std::string> trackName;
};
// --- eventlauncher → TEventLauncher (world/EvLaunch.h) ---
struct EventLauncherCondition {
std::string memcellName;
std::string compareText;
double compareValue1 = 0.0;
double compareValue2 = 0.0;
int checkMask = 0;
};
struct RuntimeEventLauncher {
BasicNode node;
Vec3 location;
double radiusSquared = 0.0;
std::string activationKeyRaw;
int activationKey = 0;
double deltaTime = -1.0;
std::string event1Name;
std::string event2Name;
std::optional<EventLauncherCondition> condition;
bool trainTriggered = false;
int launchHour = -1;
int launchMinute = -1;
};
// --- dynamic → TDynamicObject (vehicle/DynObj) ---
struct RuntimeDynamicObject {
BasicNode node;
std::string dataFolder;
std::string skinFile;
std::string mmdFile;
std::string trackName;
double offset = -1.0;
std::string driverType;
int coupling = 3;
std::string couplingRaw = "3";
std::string couplingParams;
float velocity = 0.f;
int loadCount = 0;
std::string loadType;
std::optional<std::string> destination;
std::optional<std::size_t> trainsetIndex;
};
// --- sound → sound_source (audio/sound.h) ---
struct RuntimeSoundSource {
BasicNode node;
Vec3 location;
std::string wavFile;
};
} // namespace eu07::scene::runtime

View File

@@ -0,0 +1,53 @@
#pragma once
// Zbiorczy dokument runtime po bake.
#include <eu07/scene/runtime/directives.hpp>
#include <eu07/scene/runtime/scratch.hpp>
#include <cstdint>
#include <vector>
namespace eu07::scene::runtime {
// Stare szkice tagow — nieuzywane (runtime uzywa chunkow czteroliterowych w runtime_module.hpp).
enum class SectionTag : std::uint32_t {
FileHeader = 0x45443742, // 'ED7B' placeholder
Transform = 1,
Trainset = 2,
Event = 3,
NodeModel = 10,
NodeShape = 11,
NodeLines = 12,
NodeTrack = 13,
NodeTraction = 14,
NodePowerSource = 15,
NodeMemCell = 16,
NodeEventLauncher = 17,
NodeDynamic = 18,
NodeSound = 19,
};
struct RuntimeScene {
RuntimeScratchpad scratch;
std::vector<TransformDirective> transforms;
std::vector<RuntimeTrainset> trainsets;
std::vector<RuntimeEvent> events;
std::vector<RuntimeModelInstance> models;
std::vector<RuntimeShapeNode> shapes;
std::vector<RuntimeLinesNode> lines;
std::vector<RuntimeTrack> tracks;
std::vector<RuntimeTraction> traction;
std::vector<RuntimeTractionPowerSource> powerSources;
std::vector<RuntimeMemCell> memcells;
std::vector<RuntimeEventLauncher> eventLaunchers;
std::vector<RuntimeDynamicObject> dynamics;
std::vector<RuntimeSoundSource> sounds;
// FirstInit — marker inicjalizacji scenariusza (bez payloadu)
std::uint32_t firstInitCount = 0;
};
} // namespace eu07::scene::runtime

View File

@@ -0,0 +1,38 @@
#pragma once
// scene/scene.h — scratch_data (kontekst parsowania, nie persystowany 1:1)
#include <eu07/scene/node/types.hpp>
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
namespace eu07::scene::runtime {
struct RuntimeScratchpad {
struct LocationState {
std::vector<Vec3> offsetStack;
std::vector<Vec3> scaleStack;
Vec3 rotation{};
} location;
struct TrainsetState {
bool isOpen = false;
std::string name;
std::string track;
float offset = 0.f;
float velocity = 0.f;
std::vector<std::size_t> vehicleIndices;
std::vector<int> couplings;
std::size_t driverIndex = static_cast<std::size_t>(-1);
std::unordered_map<std::string, std::string> assignment;
} trainset;
std::string scenarioName;
bool initialized = false;
bool timeInitialized = false;
};
} // namespace eu07::scene::runtime

Some files were not shown because too many files have changed in this diff Show More