16
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-07-18 00:49: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

@@ -48,6 +48,7 @@ file(GLOB HEADERS "*.h"
"global_include/*.h"
"simulation/*.h"
"scene/*.h"
"scene/eu7/*.h"
"rendering/*.h"
"audio/*.h"
"environment/*.h"
@@ -82,6 +83,7 @@ option(GENERATE_PDB "Generate executable with program debugging symbols" ON)
option(ENABLE_MCC "Enable multicore compilation" ON)
option(WITHDUMPGEN "Enable generating DMP files on crash" ON)
option(ENABLE_AVX2 "Enable AVX2 instruction set (requires AVX2-capable CPU, Haswell/2013 or newer)" ON)
option(WITH_EU7_PARSER "Embed EU07 SCM→EU7 baker (eu07-parser)" ON)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
@@ -148,6 +150,17 @@ set(SOURCES
"stdafx.cpp"
"input/messaging.cpp"
"scene/scene.cpp"
"scene/eu7/eu7_loader.cpp"
"scene/eu7/eu7_load_stats.cpp"
"scene/eu7/eu7_pack_bench.cpp"
"scene/eu7/eu7_section.cpp"
"scene/eu7/eu7_section_stream.cpp"
"scene/eu7/eu7_model_prefetch.cpp"
"scene/eu7/eu7_reader.cpp"
"scene/eu7/eu7_emit.cpp"
"scene/eu7/eu7_apply.cpp"
"scene/eu7/eu7_bake.cpp"
"scene/eu7/eu7_parameters.cpp"
"scene/scenenode.cpp"
"simulation/simulation.cpp"
"model/vertex.cpp"
@@ -392,9 +405,32 @@ if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC)
add_compile_options("/utf-8")
endif()
find_package(Threads REQUIRED)
set(EU07_BAKE_LIBS "")
if(WITH_EU7_PARSER)
set(EU07_PARSER_INCLUDE "${CMAKE_CURRENT_SOURCE_DIR}/eu07-parser/include")
if(NOT EXISTS "${EU07_PARSER_INCLUDE}/eu07/parser.hpp")
message(FATAL_ERROR "EU07 parser headers missing at ${EU07_PARSER_INCLUDE}. Sync eu07-parser/include from the parser repo.")
endif()
add_library(eu07_bake STATIC "scene/eu7/eu7_bake_parser.cpp")
target_compile_features(eu07_bake PUBLIC cxx_std_23)
set_target_properties(eu07_bake PROPERTIES CXX_STANDARD 23 CXX_STANDARD_REQUIRED ON)
target_include_directories(eu07_bake PRIVATE "${EU07_PARSER_INCLUDE}")
target_link_libraries(eu07_bake PUBLIC Threads::Threads)
if(MSVC)
target_compile_options(eu07_bake PRIVATE /utf-8 /Zc:__cplusplus /EHsc /wd4834)
endif()
list(APPEND EU07_BAKE_LIBS eu07_bake)
endif()
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
target_compile_definitions(${PROJECT_NAME} PRIVATE ${DEFINITIONS})
if(WITH_EU7_PARSER)
target_compile_definitions(${PROJECT_NAME} PRIVATE WITH_EU7_PARSER=1)
endif()
target_include_directories(${PROJECT_NAME} PRIVATE
"."
@@ -442,6 +478,7 @@ if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
target_precompile_headers(${PROJECT_NAME} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_SOURCE_DIR}/stdafx.h>")
set_source_files_properties("/glad/src/glad.c" "ref/dds-ktx/src/dds-ktx.c" "stb/stb_image.c"
"imgui/imgui.cpp" "imgui/imgui_demo.cpp" "imgui/imgui_draw.cpp" "imgui/imgui_widgets.cpp" "imgui/imgui_impl_glfw.cpp"
"scene/eu7/eu7_bake_parser.cpp"
PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE)
endif()
endif()
@@ -483,8 +520,7 @@ else()
target_link_libraries(${PROJECT_NAME} ${PNG_LIBRARIES})
endif()
find_package(Threads REQUIRED)
target_link_libraries(${PROJECT_NAME} Threads::Threads)
target_link_libraries(${PROJECT_NAME} Threads::Threads ${EU07_BAKE_LIBS})
find_package(GLM REQUIRED)
target_include_directories(${PROJECT_NAME} PRIVATE ${GLM_INCLUDE_DIR})

View File

@@ -12,6 +12,9 @@ http://mozilla.org/MPL/2.0/.
#include "application/drivermode.h"
#include "application/editormode.h"
#include "application/scenarioloadermode.h"
#include "application/scenarioloaderuilayer.h"
#include "scene/eu7/eu7_load_stats.h"
#include "scene/eu7/eu7_section_stream.h"
#include "launcher/launchermode.h"
#include "utilities/Globals.h"
@@ -707,9 +710,59 @@ void eu07_application::exit()
#endif
}
void
eu07_application::extend_loading_overlay( std::chrono::milliseconds const duration ) {
if( duration.count() <= 0 ) {
return;
}
auto const until { std::chrono::steady_clock::now() + duration };
if( until > m_loading_overlay_until ) {
m_loading_overlay_until = until;
}
if( m_loading_overlay == nullptr ) {
m_loading_overlay = std::make_shared<scenarioloader_ui>();
}
}
bool
eu07_application::loading_overlay_active() const {
if( m_modestack.empty() || m_modestack.top() != mode::driver ) {
return false;
}
if( false == simulation::is_ready ) {
return true;
}
return false;
}
void
eu07_application::sync_loading_overlay_state() {
if( m_loading_overlay == nullptr ) {
m_loading_overlay = std::make_shared<scenarioloader_ui>();
}
m_loading_overlay->set_progress( 100.f, 0.f );
}
void
eu07_application::render_loading_overlay() {
sync_loading_overlay_state();
if( m_loading_overlay == nullptr ) {
m_loading_overlay = std::make_shared<scenarioloader_ui>();
}
m_loading_overlay->render();
}
void eu07_application::render_ui()
{
if( loading_overlay_active() ) {
render_loading_overlay();
return;
}
if (m_modestack.empty())
{
return;
@@ -721,6 +774,12 @@ void eu07_application::render_ui()
void eu07_application::begin_ui_frame()
{
if( loading_overlay_active() ) {
sync_loading_overlay_state();
m_loading_overlay->begin_ui_frame();
return;
}
if (m_modestack.empty())
{
return;

View File

@@ -9,6 +9,8 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <chrono>
#include "application/applicationmode.h"
#include "scripting/PyInt.h"
#include "network/manager.h"
@@ -55,6 +57,12 @@ public:
render_ui();
void
begin_ui_frame();
void
extend_loading_overlay( std::chrono::milliseconds const Duration );
[[nodiscard]] bool
loading_overlay_active() const;
void
render_loading_overlay();
// switches application to specified mode
bool
pop_mode();
@@ -115,9 +123,12 @@ private:
int init_modes();
bool init_network();
int run_crashgui();
void sync_loading_overlay_state();
// members
bool m_screenshot_queued = false;
std::chrono::steady_clock::time_point m_loading_overlay_until {};
std::shared_ptr<class scenarioloader_ui> m_loading_overlay;
modeptr_array m_modes { nullptr }; // collection of available application behaviour modes
mode_stack m_modestack; // current behaviour mode

View File

@@ -32,7 +32,7 @@ public:
void begin_ui_frame() {
if( m_userinterface != nullptr ) {
m_userinterface->begin_ui_frame(); } }
inline
virtual
void set_progress( float const Progress = 0.f, float const Subtaskprogress = 0.f ) {
if( m_userinterface != nullptr ) {
m_userinterface->set_progress( Progress, Subtaskprogress ); } }

View File

@@ -29,6 +29,8 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/Timer.h"
#include "rendering/renderer.h"
#include "utilities/Logs.h"
#include "scene/eu7/eu7_section_stream.h"
#include "scene/eu7/eu7_pack_bench.h"
/*
namespace input {
@@ -402,6 +404,36 @@ bool driver_mode::update()
Timer::subsystem.sim_total.stop();
auto const world_presentable {
false == scene::eu7::section_stream_active()
|| scene::eu7::loading_screen_dismissed() };
auto const gameplay_stream {
scene::eu7::section_stream_active() && scene::eu7::loading_screen_dismissed() };
simulation::State.drain_deferred_eu7_trainsets( gameplay_stream ? 3.0 : 12.0 );
if( scene::eu7::section_stream_active() ) {
auto const stream_active {
simulation::is_ready || Application.loading_overlay_active() };
if( stream_active && scene::eu7::section_stream_needs_bootstrap() ) {
scene::eu7::kick_section_stream_bootstrap();
}
if( stream_active ) {
scene::eu7::update_section_stream( Global.pCamera.Pos );
scene::eu7::drain_section_stream();
}
if( scene::eu7::pack_bench_stream_phase_active() ) {
static double s_last_stream_bench_log { 0.0 };
auto const now { Timer::GetTime() };
if( now - s_last_stream_bench_log >= 30.0
&& scene::eu7::pack_bench_stream().sections_finalized > 0 ) {
scene::eu7::log_pack_stream_bench();
s_last_stream_bench_log = now;
}
}
}
simulation::Region->update_sounds();
audio::renderer.update(Global.iPause ? 0.0 : deltarealtime);
@@ -410,7 +442,9 @@ bool driver_mode::update()
GfxRenderer->Update(deltarealtime);
simulation::is_ready = simulation::is_ready || ((simulation::Train != nullptr) && (simulation::Train->is_cab_initialized)) || (Global.local_start_vehicle == "ghostview");
simulation::is_ready = simulation::is_ready
|| ( ( simulation::Train != nullptr ) && ( simulation::Train->is_cab_initialized ) )
|| ( Global.local_start_vehicle == "ghostview" && world_presentable );
return true;
}
@@ -461,7 +495,10 @@ void driver_mode::enter()
}
// maintenance method, called when the mode is deactivated
void driver_mode::exit() {}
void
driver_mode::exit() {
scene::eu7::flush_pack_stream_bench();
}
void driver_mode::on_key(int const Key, int const Scancode, int const Action, int const Mods)
{

View File

@@ -19,6 +19,15 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/renderer.h"
#include "utilities/Logs.h"
#include "utilities/translation.h"
#include "scene/eu7/eu7_load_stats.h"
#include "scene/eu7/eu7_pack_bench.h"
#include "scene/eu7/eu7_section_stream.h"
namespace {
constexpr std::chrono::seconds kEu7LoadTimeout { 90 };
constexpr float kDeserializeBarShare { 70.f };
constexpr float kPackBarShare { 30.f };
} // namespace
scenarioloader_mode::scenarioloader_mode() {
m_userinterface = std::make_shared<scenarioloader_ui>();
@@ -30,6 +39,13 @@ bool scenarioloader_mode::init() {
return true;
}
void scenarioloader_mode::update_bar_progress( float const progress ) {
m_bar_progress = std::max( m_bar_progress, std::clamp( progress, 0.f, 100.f ) );
if( m_userinterface != nullptr ) {
m_userinterface->set_progress( m_bar_progress, 0.f );
}
}
// mode-specific update of simulation data. returns: false on error, true otherwise
bool scenarioloader_mode::update() {
if (!Global.ready_to_load)
@@ -48,17 +64,80 @@ bool scenarioloader_mode::update() {
}
try {
if (simulation::State.deserialize_continue(state))
return true;
if( m_phase == load_phase::deserialize ) {
if( simulation::State.deserialize_continue( state ) ) {
return true;
}
m_phase = load_phase::eu7_load;
m_eu7_load_started = std::chrono::steady_clock::now();
m_eu7_stream_primed = false;
}
}
catch (invalid_scenery_exception &e) {
ErrorLog( "Bad init: scenario loading failed" );
Application.pop_mode();
return true;
}
if( m_phase == load_phase::eu7_load ) {
if( scene::eu7::section_stream_active() ) {
simulation::State.drain_deferred_eu7_trainsets( 16.0 );
auto const position { scene::eu7::stream_loading_position() };
if( false == m_eu7_stream_primed ) {
if( scene::eu7::section_stream_needs_bootstrap() ) {
scene::eu7::kick_section_stream_bootstrap();
}
m_eu7_stream_primed = true;
}
scene::eu7::drain_section_stream();
auto const ring_progress {
scene::eu7::section_stream_ring_progress(
position,
scene::eu7::kSectionStreamBootstrapRadiusKm ) };
update_bar_progress(
kDeserializeBarShare + ring_progress * kPackBarShare );
auto const timed_out {
std::chrono::steady_clock::now() - m_eu7_load_started >= kEu7LoadTimeout };
auto const bootstrap_ready {
scene::eu7::section_stream_ready_around(
position,
scene::eu7::kSectionStreamBootstrapRadiusKm ) };
if(
bootstrap_ready
|| scene::eu7::section_stream_presentable_around(
position,
scene::eu7::kSectionStreamBootstrapRadiusKm ) ) {
WriteLog( "EU7 PACK: pierścień wokół pozycji startowej gotowy do pokazania" );
scene::eu7::dismiss_loading_screen();
m_phase = load_phase::finished;
}
else if( timed_out ) {
ErrorLog(
"EU7 PACK: timeout ładowania pierścienia wokół kamery — wchodzę w jazdę (ring=" +
std::to_string( static_cast<int>( ring_progress * 100.f ) ) + "%)" );
scene::eu7::dismiss_loading_screen();
m_phase = load_phase::finished;
}
else {
return true;
}
}
else {
scene::eu7::dismiss_loading_screen();
m_phase = load_phase::finished;
}
}
WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast<std::chrono::seconds>( ( std::chrono::system_clock::now() - timestart ) ).count() ) + " seconds" );
// TODO: implement and use next mode cue
scene::eu7::log_load_stats();
scene::eu7::log_pack_bench();
update_bar_progress( 100.f );
Application.pop_mode();
Application.push_mode( eu07_application::mode::driver );
@@ -74,6 +153,9 @@ void scenarioloader_mode::enter() {
// TBD: hide cursor in fullscreen mode?
Application.set_cursor( GLFW_CURSOR_NORMAL );
m_phase = load_phase::deserialize;
m_eu7_stream_primed = false;
m_bar_progress = 0.f;
simulation::is_ready = false;
Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" );
@@ -85,3 +167,15 @@ void scenarioloader_mode::exit() {
simulation::Time.init( Global.starting_timestamp );
simulation::Environment.init();
}
void scenarioloader_mode::set_progress(
float const Progress,
float const Subtaskprogress ) {
if( m_phase != load_phase::deserialize ) {
return;
}
auto const parser_progress {
std::max( Progress, Subtaskprogress ) };
update_bar_progress( parser_progress * kDeserializeBarShare / 100.f );
}

View File

@@ -13,8 +13,21 @@ http://mozilla.org/MPL/2.0/.
#include "simulation/simulation.h"
class scenarioloader_mode : public application_mode {
enum class load_phase {
deserialize,
eu7_load,
finished
};
std::shared_ptr<simulation::deserializer_state> state;
std::chrono::system_clock::time_point timestart;
load_phase m_phase { load_phase::deserialize };
std::chrono::steady_clock::time_point m_eu7_load_started {};
bool m_eu7_stream_primed { false };
float m_bar_progress { 0.f };
void update_bar_progress( float progress );
public:
// constructors
@@ -28,6 +41,7 @@ public:
void enter() override;
// maintenance method, called when the mode is deactivated
void exit() override;
void set_progress( float Progress = 0.f, float Subtaskprogress = 0.f ) override;
// input handlers
void on_key( int const Key, int const Scancode, int const Action, int const Mods ) override { ; }
void on_cursor_pos( double const Horizontal, double const Vertical ) override { ; }

View File

@@ -195,6 +195,10 @@ void scenarioloader_ui::render_()
ImGui::SetNextWindowSize(ImVec2(Global.window_size.x + padding * 2, Global.window_size.y + padding * 2));
ImGui::Begin("Neo Loading Screen", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddRectFilled(
ImVec2( 0.f, 0.f ),
ImVec2( screen_size.x, screen_size.y ),
IM_COL32( 0, 0, 0, 255 ) );
ImGui::PushFont(font_loading);
ImGui::SetWindowFontScale(1);
const float font_scale_mult = 48 / ImGui::GetFontSize();
@@ -228,7 +232,14 @@ void scenarioloader_ui::render_()
icon_center_y - text_size.y * 0.5f);
// Draw
//draw_list->AddImage(reinterpret_cast<ImTextureID>(loading_tex), icon_pos, ImVec2(icon_pos.x + loading_size.x, icon_pos.y + loading_size.y), ImVec2(0, 0), ImVec2(1, 1));
if( loading_tex != static_cast<GLuint>( -1 ) ) {
draw_list->AddImage(
reinterpret_cast<ImTextureID>( loading_tex ),
icon_pos,
ImVec2( icon_pos.x + loading_size.x, icon_pos.y + loading_size.y ),
ImVec2( 0, 0 ),
ImVec2( 1, 1 ) );
}
draw_list->AddText(text_pos, IM_COL32_WHITE, m_progresstext.c_str());
// Trivia

951
docs/eu7-format.md Normal file
View File

@@ -0,0 +1,951 @@
# Format pliku EU7B (`.eu7`)
> Dokumentacja oparta na kodzie źródłowym z:
> - `scene/eu7/` — runtime reader/loader (maszyna-fresh)
> - `parser/include/eu07/scene/binary/` — writer/reader (narzędzie CLI)
>
> Stan: **czerwiec 2026**, obejmuje wersje v4v8.
---
## Spis treści
1. [Przegląd ogólny](#1-przegląd-ogólny)
2. [Nagłówek pliku i wersje](#2-nagłówek-pliku-i-wersje)
3. [Struktura chunku](#3-struktura-chunku)
4. [Kolejność i reguły chunkowania](#4-kolejność-i-reguły-chunkowania)
5. [Typy podstawowe i kodowanie](#5-typy-podstawowe-i-kodowanie)
6. [Slim Node — wspólny nagłówek węzła](#6-slim-node--wspólny-nagłówek-węzła)
7. [Transform Context](#7-transform-context)
8. [Packed vertex — pakowany wierzchołek](#8-packed-vertex--pakowany-wierzchołek)
9. [Lighting block — blok oświetlenia](#9-lighting-block--blok-oświetlenia)
10. [Catalog chunkowy — zestawienie](#10-catalog-chunkowy--zestawienie)
11. [STRS — tablica łańcuchów](#11-strs--tablica-łańcuchów)
12. [INCL — referencje do podmodułów](#12-incl--referencje-do-podmodułów)
13. [TRAK — tory](#13-trak--tory)
14. [TRAC — przewody trakcyjne](#14-trac--przewody-trakcyjne)
15. [PWRS — źródła zasilania trakcji](#15-pwrs--źródła-zasilania-trakcji)
16. [TERR — teren (terrain shapes)](#16-terr--teren-terrain-shapes)
17. [MESH — kształty geometryczne](#17-mesh--kształty-geometryczne)
18. [LINE — linie](#18-line--linie)
19. [MODL — instancje modeli 3D](#19-modl--instancje-modeli-3d)
20. [MEMC — komórki pamięci](#20-memc--komórki-pamięci)
21. [LAUN — wyzwalacze zdarzeń](#21-laun--wyzwalacze-zdarzeń)
22. [DYNM — pojazdy dynamiczne](#22-dynm--pojazdy-dynamiczne)
23. [SOND — dźwięki otoczenia](#23-sond--dźwięki-otoczenia)
24. [TRSE — zestawy wagonowe](#24-trse--zestawy-wagonowe)
25. [EVNT — zdarzenia](#25-evnt--zdarzenia)
26. [FINT — licznik first-init](#26-fint--licznik-first-init)
27. [PLAC — parametry umieszczenia include](#27-plac--parametry-umieszczenia-include)
28. [PIDX — indeks sekcji PACK (v7+)](#28-pidx--indeks-sekcji-pack-v7)
29. [PACK — instancje modeli per sekcja 1 km (v7+)](#29-pack--instancje-modeli-per-sekcja-1-km-v7)
30. [PROT — prototypy modeli (v8+)](#30-prot--prototypy-modeli-v8)
31. [Semantyka strumieniowania PACK w runtime](#31-semantyka-strumieniowania-pack-w-runtime)
32. [Historia wersji](#32-historia-wersji)
33. [Ścieżki plików i konwencje nazewnictwa](#33-ścieżki-plików-i-konwencje-nazewnictwa)
---
## 1. Przegląd ogólny
Plik `.eu7` (EU7B — **EU7 Binary**) to binarny format zapisu skompilowanego modułu scenerii symulatora EU07. Zastępuje tekst source (`*.scm`, `*.inc`, `*.scn`) jednym zoptymalizowanym plikiem binarnym gotowym do szybkiego wczytania przez silnik.
**Kluczowe cechy:**
- **Little-endian** dla wszystkich liczb całkowitych i zmiennoprzecinkowych
- **Chunki samoopisowe** — nieznane chunki mogą być pomijane (`seekg` po `chunk_size`)
- **Tablica łańcuchów (STRS)** — wszystkie stringi współdzielone przez indeks `uint32`
- **Strumieniowanie sekcji 1 km** — w plikach głównych scenariusza modele są dzielone na kafelki 1 km × 1 km (chunki PIDX + PACK), wczytywane asynchronicznie
- **Format skaluje się** od małych modułów include (kilka KB) do głównych scenariuszy (setki MB w PACK)
---
## 2. Nagłówek pliku i wersje
### Layout nagłówka
```
Offset Rozmiar Typ Opis
------ ------- -------- -------------------------
0 4 char[4] Magic = 'EU7B' (0x45 0x55 0x37 0x42)
4 4 uint32 Version (patrz tabela poniżej)
8 ... chunks Strumień chunkowy (do EOF)
```
Nie ma pola "łączny rozmiar pliku" w nagłówku. Po nagłówku następują chunki aż do EOF.
### Tabela wersji
| Wartość | Stała (maszyna-fresh) | Stała (parser CLI) | Opis |
|---------|-------------------------|---------------------------|----------------------------------------------------------------|
| `1` | — | `kVersionLegacy` | Legacy — **nieobsługiwane** w CLI ani runtime |
| `2` | — | `kVersionRuntimeF64` | Skalary i Vec3 jako `double` (float64) na dysku |
| `3` | — | `kVersionRuntimeF32` | Skalary i Vec3 jako `float32` na dysku |
| `4` | `kEu7VersionV4` | `kVersionRuntimeV4` | Slim node + packed mesh (snorm16 normy + half-float UV) |
| `5` | `kEu7VersionV5` | `kVersionRuntimeV5` | Adds TERR z batched terrain per sekcja 1 km |
| `6` | `kEu7VersionV6` | `kVersionRuntime` | Adds site transform w rekordach INCL |
| `7` | `kEu7VersionV7` | `kVersionRuntimeV7` | Adds PIDX + PACK — modele scenerii per sekcja 1 km (streaming) |
| `8` | `kEu7VersionV8` | — | Adds PROT (prototypy) + nowy format sekcji PACK v8 |
**Runtime maszyna-fresh obsługuje wersje 48.** Wersje 13 nie są obsługiwane przez żaden z dostępnych loaderów.
---
## 3. Struktura chunku
```
Offset Rozmiar Typ Opis
------ ------- ------- -----------------------------------------
0 4 uint32 Chunk ID — FourCC little-endian (4 znaki ASCII)
4 4 uint32 Chunk total size — ŁĄCZNIE z nagłówkiem (≥ 8)
8 ... bytes Payload (rozmiar = chunk_total_size - 8)
```
> **Uwaga:** pole `chunk_total_size` **wlicza** 8 bajtów własnego nagłówka. Aby obliczyć rozmiar payloadu: `payload_size = chunk_total_size - 8`.
Strumień chunkowy nie ma terminator record. Parsowanie trwa do momentu napotkania EOF.
### FourCC — sposób kodowania
ID chunku jest 4-znakowym ASCII wpisanym w kolejności little-endian do `uint32`:
```cpp
constexpr uint32_t make_id4(char a, char b, char c, char d) {
return (uint32(d) << 24) | (uint32(c) << 16) | (uint32(b) << 8) | uint32(a);
}
// Np. kChunkPack = make_id4('P','A','C','K')
// Bytes w pliku: 50 41 43 4B → odczytane jako uint32 LE = 0x4B434150
```
Przy odczycie człowiek widzi w hex-dumpie bajty w kolejności `P A C K`, co odpowiada oczytaniu `'PACK'` wprost.
---
## 4. Kolejność i reguły chunkowania
1. **STRS** musi wystąpić **przed** wszystkimi chunkiami używającymi indeksów string (w praktyce jest zawsze pierwszym chunkiem).
2. **PACK** jest **pomijany przy pełnym parse** — runtime zapamiętuje jedynie offset początku payloadu i rozmiar. Faktyczne dane są czytane on-demand przez `read_pack_section()`.
3. **PROT** musi wystąpić **przed** PACK (prototypy muszą być załadowane do pamięci zanim sekcje PACK będą rozwijane).
4. **PIDX** musi wystąpić przed próbą streamingu (runtime czyta PIDX przy załadowaniu modułu).
5. Nierozpoznane chunki są **ignorowane** — reader skacze do `chunk_start + payload_size`.
---
## 5. Typy podstawowe i kodowanie
Wszystkie typy całkowite i zmiennoprzecinkowe — **little-endian**.
| Typ w pliku | C++ typ docelowy | Rozmiar | Opis |
|-------------|------------------|---------|---------------------------------------------------|
| `uint8` | `uint8_t` | 1 B | Bajt bez znaku |
| `int16` | `int16_t` | 2 B | LE, ze znakiem |
| `uint16` | `uint16_t` | 2 B | LE, bez znaku |
| `int32` | `int32_t` | 4 B | LE, ze znakiem |
| `uint32` | `uint32_t` | 4 B | LE, bez znaku |
| `uint64` | `uint64_t` | 8 B | LE, bez znaku (lo-word najpierw) |
| `float32` | `float` | 4 B | IEEE 754 single precision |
| `f64_disk` | `double` | 4 B | **float32 na dysku** — wczytywany jako double w RAM |
| `Vec3` | `glm::dvec3` | 12 B | 3× `f64_disk` (= 3× float32, razem 12 B) |
| `snorm16` | `float` | 2 B | int16 / 32767.0 → zakres [-1, +1] |
| `half16` | `float` | 2 B | IEEE 754 half precision |
| `string_id` | `uint32_t` | 4 B | Indeks do tablicy STRS; `0xFFFFFFFF` = pusty |
> **`f64_disk`**: Od wersji v3 wzwyż skalary (dystanse, kąty, napięcia itp.) i współrzędne Vec3 są zapisywane jako **float32** (4 B), mimo że runtime przechowuje je jako `double`. Patrz: `io::writeF64()` w `io.hpp` — wewnętrznie rzutuje do `float` przed zapisem.
---
## 6. Slim Node — wspólny nagłówek węzła
Każdy obiekt sceny (tor, model, dźwięk itp.) poprzedzony jest **slim node** — skompresowanym nagłówkiem z opcjonalnymi polami sterowanymi bajtem flag.
### Layout
```
Offset Rozmiar Typ Opis
------ ------- --------- -------------------------------------------
0 1 uint8 flags (bitmaska, patrz poniżej)
--- poniższe pola są OPCJONALNE, zależne od flags ---
+0 4 string_id name (jeśli bit 0 set)
+0 4 f64_disk range_squared_min (jeśli bit 1 set)
+0 4 f64_disk range_squared_max (jeśli bit 2 set; domyślnie +∞)
+0 16 Vec3+f32 bounding sphere: center(Vec3) + radius(float32) (jeśli bit 3)
+0 4 uint32 group_handle (jeśli bit 4 set)
+0 ... Transform transform context (jeśli bit 5 set)
```
### Bitmaska `flags`
| Bit | Stała | Znaczenie |
|-----|--------------------------|-----------------------------------------------------|
| 0 | `kNodeFlagHasName` | pole `name` (string_id) jest obecne |
| 1 | `kNodeFlagHasRangeMin` | pole `range_squared_min` (f64_disk) jest obecne |
| 2 | `kNodeFlagHasRangeMax` | pole `range_squared_max` jest obecne (inaczej +∞) |
| 3 | `kNodeFlagHasBounds` | bounding sphere: Vec3 center + float32 radius |
| 4 | `kNodeFlagHasGroup` | pole `group_handle` (uint32) jest obecne |
| 5 | `kNodeFlagHasTransform` | transform context jest obecny (patrz sekcja 7) |
| 6 | `kNodeFlagNotVisible` | węzeł domyślnie niewidoczny (`visible = false`) |
### Bounding sphere
```
Vec3 center (12 B)
float32 radius (4 B)
```
Łącznie 16 B, obecne tylko gdy bit 3 set.
---
## 7. Transform Context
Kontekst transformacji lokacji węzła — stos offsetów i skal + obrót.
```
Offset Rozmiar Typ Opis
------ ---------------- -------- ----------------------------------------
0 1 uint8 origin_count (maks. 255)
1 12 × origin_count Vec3[] origin_stack (offsety translacji)
+ 1 uint8 scale_count (maks. 255)
+ 12 × scale_count Vec3[] scale_stack (skala)
+ 12 Vec3 rotation (kąty Eulera w stopniach, XYZ)
+ 1 uint8 group_depth
```
Kolejność stosowania transformacji (w writer: `transform_point()`):
1. Obrót Y (o `rotation.y` stopni)
2. Skalowanie (`scale_stack.back()`)
3. Translacja (`origin_stack.back()`)
---
## 8. Packed vertex — pakowany wierzchołek
Używany w chunkach MESH i TERR (od v4).
```
Offset Rozmiar Typ Opis
------ ------- ------- ------------------------------------------
0 4 float32 position.x
4 4 float32 position.y
8 4 float32 position.z
12 2 int16 normal.x jako snorm16 (/ 32767)
14 2 int16 normal.y jako snorm16
16 2 int16 normal.z jako snorm16
18 2 uint16 u jako half-float (IEEE 754 fp16)
20 2 uint16 v jako half-float
```
Łącznie: **22 bajty** na wierzchołek.
W chunkach LINE wierzchołki to **tylko pozycja** (Vec3 = 12 B; brak normalnych i UV).
---
## 9. Lighting block — blok oświetlenia
Opcjonalny blok oświetlenia materiału węzła (48 bajtów).
```
Offset Rozmiar Typ Opis
------ ------- ------- ------------------
0 16 4×f32 diffuse RGBA
16 16 4×f32 ambient RGBA
32 16 4×f32 specular RGBA
```
Domyślne wartości (gdy brak bloku):
- `diffuse = (0.8, 0.8, 0.8, 1.0)`
- `ambient = (0.2, 0.2, 0.2, 1.0)`
- `specular = (0.0, 0.0, 0.0, 1.0)`
Blok jest poprzedzony znacznikiem `uint8` (`0` = brak, `!0` = blok obecny) w rekordach MESH i LINE.
---
## 10. Catalog chunkowy — zestawienie
| FourCC | Stała C++ | Wersja min | Opis |
|---------|------------------|------------|------------------------------------------------|
| `STRS` | `kChunkStrs` | v4 | Tablica shared strings (musi być pierwsza) |
| `INCL` | `kChunkIncl` | v4 | Referencje do submodułów `.eu7` |
| `TRAK` | `kChunkTrak` | v4 | Tory (Track) |
| `TRAC` | `kChunkTrac` | v4 | Przewody trakcyjne (Traction) |
| `PWRS` | `kChunkPwrs` | v4 | Źródła zasilania trakcji |
| `TERR` | `kChunkTerr` | v5 | Teren (batched triangles per sekcja) |
| `MESH` | `kChunkMesh` | v4 | Kształty geometryczne (triangles/strip/fan) |
| `LINE` | `kChunkLine` | v4 | Linie (lines/strip/loop) |
| `MODL` | `kChunkModl` | v4 | Instancje modeli 3D (flat list) |
| `MEMC` | `kChunkMemc` | v4 | Komórki pamięci (MemCell) |
| `LAUN` | `kChunkLaun` | v4 | Wyzwalacze zdarzeń (EventLauncher) |
| `DYNM` | `kChunkDynm` | v4 | Pojazdy dynamiczne (Dynamic) |
| `SOND` | `kChunkSond` | v4 | Dźwięki otoczenia (Sound) |
| `TRSE` | `kChunkTrset` | v4 | Zestawy wagonowe (Trainset) |
| `EVNT` | `kChunkEvnt` | v4 | Zdarzenia (Event) |
| `FINT` | `kChunkFint` | v4 | Licznik obiektów first-init |
| `PLAC` | `kChunkPlac` | v4 | Parametry umieszczenia include |
| `PIDX` | `kChunkPidx` | **v7** | Indeks sekcji 1 km → offset w PACK |
| `PACK` | `kChunkPack` | **v7** | Strumień modeli per sekcja 1 km |
| `PROT` | `kChunkProt` | **v8** | Prototypy modeli (współdzielone definicje) |
---
## 11. STRS — tablica łańcuchów
Wszystkie łańcuchy tekstu w pliku są przechowywane w jednej tablicy i referowane przez `string_id` (indeks `uint32`). Specjalny indeks `0xFFFFFFFF` oznacza pusty string.
```
uint32 count
Powtórzone count razy:
uint32 length (w bajtach, bez null-terminatora)
char[] data (length bajtów UTF-8/ASCII, bez null)
```
Każdy kolejny string ma numer indeksu 0, 1, 2, …
---
## 12. INCL — referencje do podmodułów
Przechowuje listę `include` wskazujących na inne pliki `.eu7` — submoduły scenerii.
```
uint32 count
Powtórzone count razy:
uint32 source_line — numer linii w pliku tekstowym źródłowym
string_id source_path — ścieżka do pliku tekstowego (.scm/.inc)
string_id binary_path — ścieżka do pliku .eu7
uint32 param_count
string_id[] parameters — param_count × string_id (parametry include)
[v6+] TransformContext site_transform (nieobecne w v4/v5)
```
`site_transform` przechowuje pełny kontekst transformacji miejsca osadzenia (origin/scale/rotation) i jest używany do złożenia transformacji przy nakładaniu submodułu.
---
## 13. TRAK — tory
```
uint32 count
Powtórzone count razy:
SlimNode node
uint8 track_type — patrz enum Eu7TrackType
uint8 category — patrz enum Eu7TrackCategory
float32 length [m]
float32 track_width [m]
float32 friction
float32 sound_distance
int32 quality_flag
int32 damage_flag
uint8 environment — wartość enum + 1 (Unknown = 0, Flat = 1, ...)
uint8 has_visibility — 0 = brak, ≠0 = blok widoczności następuje
[jeśli has_visibility]:
string_id material1
float32 tex_length
string_id material2
float32 tex_height1
float32 tex_width
float32 tex_slope
uint32 path_count (maks. 65536)
Powtórzone path_count razy:
Vec3 p_start
f64_disk roll_start [stopnie]
Vec3 cp_out — punkt kontrolny wyjściowy Béziera
Vec3 cp_in — punkt kontrolny wejściowy Béziera
Vec3 p_end
f64_disk roll_end
f64_disk radius [m], 0 = prosta
uint32 tail_count (maks. 256)
Powtórzone tail_count razy:
uint8 code — 118 = predefiniowane kw., 255 = custom
[jeśli code==255]: string_id custom_key
string_id value
```
### Enum Eu7TrackType (uint8)
| Wartość | Nazwa |
|---------|-------------|
| 0 | Unknown |
| 1 | Normal |
| 2 | Switch |
| 3 | Table |
| 4 | Cross |
| 5 | Tributary |
### Enum Eu7TrackCategory (uint8)
| Wartość | Nazwa |
|---------|-------|
| 1 | Rail |
| 2 | Road |
| 4 | Water |
### Enum Eu7TrackEnvironment (uint8 w pliku = enum + 1)
| Wartość w pliku | Enum | Nazwa |
|-----------------|-------|-----------|
| 0 | -1 | Unknown |
| 1 | 0 | Flat |
| 2 | 1 | Mountains |
| 3 | 2 | Canyon |
| 4 | 3 | Tunnel |
| 5 | 4 | Bridge |
| 6 | 5 | Bank |
### Kody tail keywords (uint8)
| Kod | Słowo kluczowe |
|-----|----------------|
| 1 | `event0` |
| 2 | `eventall0` |
| 3 | `event1` |
| 4 | `eventall1` |
| 5 | `event2` |
| 6 | `eventall2` |
| 7 | `velocity` |
| 8 | `isolated` |
| 9 | `overhead` |
| 10 | `vradius` |
| 11 | `railprofile` |
| 12 | `trackbed` |
| 13 | `friction` |
| 14 | `fouling1` |
| 15 | `fouling2` |
| 16 | `sleepermodel` |
| 17 | `angle1` |
| 18 | `angle2` |
| 255 | *custom* |
---
## 14. TRAC — przewody trakcyjne
```
uint32 count
Powtórzone count razy:
SlimNode node
string_id power_supply_name
uint8 material — Eu7TractionWireMaterial (0=None,1=Copper,2=Aluminium)
float32 nominal_voltage [V]
float32 max_current [A]
float32 resistivity_ohm_per_m
f64_disk resistivity_legacy
string_id material_raw — oryginalny string materiału ("cu", "al", ...)
float32 wire_thickness [m]
int32 damage_flag
Vec3 wire_p1
Vec3 wire_p2
Vec3 wire_p3
Vec3 wire_p4
f64_disk min_height [m]
f64_disk segment_length [m]
int32 wire_count
float32 wire_offset
uint8 has_parallel — 0 = brak, ≠0 = nazwa równoległego sekcji
[jeśli has_parallel]:
string_id parallel_name
```
---
## 15. PWRS — źródła zasilania trakcji
```
uint32 count
Powtórzone count razy:
SlimNode node
Vec3 position
float32 nominal_voltage [V]
float32 voltage_frequency [Hz]
f64_disk internal_resistance_legacy
float32 internal_resistance [Ω]
float32 max_output_current [A]
float32 fast_fuse_timeout [s]
float32 fast_fuse_repetition [s]
float32 slow_fuse_timeout [s]
uint8 modifier — Eu7PowerSourceModifier (0=None,1=Recuperation,2=Section)
```
---
## 16. TERR — teren (terrain shapes)
Chunk TERR przechowuje trójkąty terenu. Może się pojawić wielokrotnie (po jednym per materiał lub per sekcja).
```
uint8 flags — bitmaska:
bit 0: translucent
bit 1: non-default lighting (blok lighting poniżej)
bit 2: batched (grupowany per sekcja 1km)
string_id material
[jeśli bit 1 set]:
LightingBlock lighting (48 B)
[jeśli bit 2 set — tryb batched]:
uint32 batch_count
Powtórzone batch_count razy:
int32 section_x_coord — współrzędna X sekcji (odczytane, ale ignorowane przez runtime)
int32 section_z_coord
uint32 vertex_count — musi być wielokrotnością 3
PackedVertex[] vertices — vertex_count wierzchołków
[else — tryb legacy]:
uint32 count — liczba trójkątów
Powtórzone count razy:
PackedVertex[3] vertices — dokładnie 3 wierzchołki (1 trójkąt)
```
> **Uwaga:** tryb batched (bit 2) jest charakterystyczny dla v5+. Pola `section_x_coord` i `section_z_coord` są odczytywane przez reader, ale ich wartości są ignorowane w runtime maszyna-fresh (batche traktowane jako płaskie listy kształtów).
---
## 17. MESH — kształty geometryczne
```
uint32 count
Powtórzone count razy:
uint8 subtype — 0=triangles, 1=triangle_strip, 2=triangle_fan
SlimNode node — typ węzła wyznaczony przez subtype
uint8 translucent — 0/1
string_id material_path
uint8 has_lighting — 0/1
[jeśli has_lighting]:
LightingBlock lighting
Vec3 origin — lokalny punkt odniesienia wierzchołków
uint32 vertex_count
PackedVertex[] vertices
```
Nazwa węzła (`node.node_type`) wyznaczona przez subtype: `"triangles"`, `"triangle_strip"`, `"triangle_fan"`.
---
## 18. LINE — linie
```
uint32 count
Powtórzone count razy:
uint8 subtype — 0=lines, 1=line_strip, 2=line_loop
SlimNode node
uint8 has_lighting — 0/1
[jeśli has_lighting]:
LightingBlock lighting
float32 line_width
Vec3 origin
uint32 vertex_count
Powtórzone vertex_count razy:
Vec3 position — TYLKO pozycja (brak normalnych/UV)
```
---
## 19. MODL — instancje modeli 3D
Używane w submodułach include. W plikach głównych z PACK chunk, MODL z podmodułów jest zazwyczaj pomijane (sterowane flagą `pack_scenery_active()`).
```
uint32 count
Powtórzone count razy:
SlimNode node
uint8 is_terrain — 0/1
uint8 transition — 0/1 (animowany przejazd)
Vec3 location — world-space, 3×float32
Vec3 angles — kąty Eulera (stopnie), 3×float32
Vec3 scale — skala XYZ, domyślnie (1,1,1)
string_id model_file — ścieżka do pliku .e3d
string_id texture_file — override tekstury (opcjonalnie pusty)
uint32 light_count
float32[] light_states — light_count wartości
uint32 color_count
uint32[] light_colors — color_count kolorów RGBA packed
```
### Aplikacja w runtime (maszyna-fresh)
Rekord MODL/PACK nie trafia przez parser tekstowy SCM. Ścieżka **instancingu GPU** (nie per-model drip):
1. `preload_unique_pack_meshes()` — jeden `GetModel()` na unikalny plik `.e3d` w całej sekcji
2. Dla każdego rekordu: `TAnimModel` + `LoadEu7()` (cache hit → szybkie `Init()`)
3. `update_instanceable_flag()` — modele bez świateł/animacji → `m_instanceable = true`
4. `Region->insert()``m_instancebuckets_opaque` → renderer: **`Render_Instanced()`**
PACK **nie** rejestruje instancji w `Instances` / `Hierarchy` (anonimowa sceneria, bez eventów po nazwie).
Deduplikacja meshów: **`TModelsManager`** (współdzielony `TModel3d`). GPU instancing: **`opengl33_renderer::Render_Instanced`** — jeden draw na submodel × N instancji tego samego mesha.
---
## 20. MEMC — komórki pamięci
```
uint32 count
Powtórzone count razy:
SlimNode node
string_id text
f64_disk value1
f64_disk value2
string_id track_name — 0xFFFFFFFF jeśli brak
```
---
## 21. LAUN — wyzwalacze zdarzeń
```
uint32 count
Powtórzone count razy:
SlimNode node
Vec3 location
f64_disk radius_squared [m²]
string_id activation_key_raw
int32 activation_key
f64_disk delta_time [s], -1 = bez limitu
string_id event1_name
string_id event2_name
int32 launch_hour — -1 = bez warunku godzinowego
int32 launch_minute
uint8 has_condition — 0/1
[jeśli has_condition]:
string_id memcell_name
string_id compare_text
f64_disk compare_value1
f64_disk compare_value2
int32 check_mask
uint8 train_triggered — 0/1
```
---
## 22. DYNM — pojazdy dynamiczne
```
uint32 count
Powtórzone count razy:
SlimNode node
string_id data_folder
string_id skin_file
string_id mmd_file
string_id track_name
string_id driver_type
string_id load_type
string_id coupling_params
string_id coupling_raw — tekstowa reprezentacja sprzęgu (np. "3.automat")
f64_disk offset [m od początku toru], -1 = auto
int32 coupling — wartość sprzęgu (0-3)
int32 load_count
float32 velocity [km/h]
uint8 has_destination — 0/1
[jeśli has_destination]:
string_id destination
uint8 has_trainset_index — 0/1
[jeśli has_trainset_index]:
uint32 trainset_index
```
---
## 23. SOND — dźwięki otoczenia
```
uint32 count
Powtórzone count razy:
SlimNode node
Vec3 location
string_id wav_file
```
---
## 24. TRSE — zestawy wagonowe
```
uint32 count
Powtórzone count razy:
string_id name
string_id track
float32 offset [m]
float32 velocity [km/h]
uint32 assignment_count
Powtórzone assignment_count razy:
string_id key
string_id value
uint32 vehicle_count
uint32[] vehicle_indices — indeksy do tablicy DYNM w module
uint32 coupling_count
int32[] couplings
uint32 driver_index — indeks pojazdu-prowadzącego
```
---
## 25. EVNT — zdarzenia
```
uint32 count
Powtórzone count razy:
string_id name
uint8 type — Eu7EventType (patrz tabela)
f64_disk delay [s]
uint32 target_count
string_id[] targets
f64_disk delay_random [s]
f64_disk delay_departure [s]
uint8 ignored — 0/1
uint8 passive — 0/1
uint32 payload_count
Powtórzone payload_count razy:
string_id key
string_id value
```
### Enum Eu7EventType (uint8)
| Wartość | Nazwa |
|---------|---------------|
| 0 | AddValues |
| 1 | UpdateValues |
| 2 | CopyValues |
| 3 | GetValues |
| 4 | PutValues |
| 5 | Whois |
| 6 | LogValues |
| 7 | Multiple |
| 8 | Switch |
| 9 | TrackVel |
| 10 | Sound |
| 11 | Texture |
| 12 | Animation |
| 13 | Lights |
| 14 | Voltage |
| 15 | Visible |
| 16 | Friction |
| 17 | Message |
| 18 | Unknown |
---
## 26. FINT — licznik first-init
Prosty licznik — ile obiektów wymaga obsługi pierwszego inicjowania.
```
uint32 first_init_count
```
---
## 27. PLAC — parametry umieszczenia include
Mapowanie parametrów wywołania include na składowe transformacji.
```
uint8 origin_x_param — numer parametru dla origin.x (0 = brak)
uint8 origin_y_param — numer parametru dla origin.y
uint8 origin_z_param — numer parametru dla origin.z
uint8 rotation_y_param — numer parametru dla rotation.y
```
Gdy wszystkie cztery pola są `0`, plik nie eksponuje parametryzowanego umieszczenia.
---
## 28. PIDX — indeks sekcji PACK (v7+)
Katalog mapujący sekcje 1 km × 1 km na offsety w payloadzie chunku PACK.
```
uint32 count
Powtórzone count razy:
uint16 row — indeks wiersza sekcji (siatka regionu)
uint16 column — indeks kolumny sekcji
uint32 model_count — liczba modeli w tej sekcji
uint64 pack_offset — offset w bajtach od początku payloadu PACK
```
**Uwagi:**
- `pack_offset` jest **względny** od początku payloadu PACK (nie od początku pliku).
- Sekcje bez modeli mogą mieć `model_count = 0` i być pominięte w PIDX.
- Indeksowanie sekcji: `row/column` odpowiadają siatce `kRegionSideSectionCount × kRegionSideSectionCount`, centrum siatki to `(0, 0)` w world-space.
- Runtime oblicza absolutny offset jako: `pack_payload_offset + entry.pack_offset`.
---
## 29. PACK — instancje modeli per sekcja 1 km (v7+)
Payload PACK jest **zbiorem sekcji** ustawionych sekwencyjnie wg offsetów z PIDX. Chunk PACK jest pomijany przy pełnym wczytaniu — reader zapamiętuje jedynie `pack_payload_offset`.
### Format sekcji — v7 (domyślny)
Każda sekcja to `model_count` rekordów `RuntimeModelInstance` (identyczny format jak rekordy MODL):
```
Powtórzone model_count razy:
(pełny rekord RuntimeModelInstance — patrz sekcja 19 MODL)
```
Transformacja węzła (`node.transform`) jest **zerowana** po wczytaniu — dane w PACK są zawsze w world-space (transform był bake'owany przy generacji).
### Format sekcji — v8 (`kPackSectionFormatV8 = 1`)
```
uint8 section_format — musi być = 1
uint32 solo_count — modele pełne (bez prototypu)
uint32 inst_count — instancje odwołujące się do PROT
Powtórzone solo_count razy:
(pełny rekord RuntimeModelInstance, transformacja zerowana)
Powtórzone inst_count razy:
uint32 proto_id — indeks do tablicy PROT
Vec3 location
Vec3 angles
Vec3 scale
string_id name
```
Instancje v8 są rozwijane przez `expand_prototype_instance()` łącząc dane z PROT z lokalizacją instancji.
---
## 30. PROT — prototypy modeli (v8+)
Wspólne definicje modeli (bez informacji o lokalizacji instancji). Muszą być obecne przed odczytaniem sekcji PACK v8.
```
uint32 count
Powtórzone count razy:
SlimNode node — name/range/bounds jak MODL, ale BEZ location/angles/scale
uint8 is_terrain
uint8 transition
string_id model_file
string_id texture_file
uint32 light_count
float32[] light_states
uint32 color_count
uint32[] light_colors
```
Różnica względem MODL: brak pól `location`, `angles`, `scale` — te są zapisane per instancja w PACK.
---
## 31. Semantyka strumieniowania PACK w runtime
Gdy root scenariusz zawiera chunk PACK, silnik maszyna-fresh uruchamia asynchroniczny **sekcja-stream**:
### Parametry strumienia
| Parametr | Wartość | Opis |
|----------------------------|------------------|----------------------------------------------------|
| `kSectionSizeM` | 1000 m | Rozmiar sekcji |
| `kInitialBootstrapRadius` | 3 sekcje | Promień wstępnego ładowania wokół pozycji startowej|
| `kStreamRadius` | 9 sekcji | Bieżący promień streamingu |
| `kMovementLookahead` | 4 sekcje | Prefetch w kierunku jazdy |
| `kMaxInFlightSections` | 72 | Maks. sekcji w toku (backpressure) |
| `kMaxReadySections` | 36 | Maks. gotowych sekcji w kolejce |
| `kBootstrapTimeoutMs` | 120 000 ms | Timeout bootstrapu (tylko ścieżka blokująca) |
W **jazdzie** (`driver_mode`): **jedna sekcja PACK na klatkę**`drain_section_stream()``apply_pending_section()` (cała sekcja naraz, bez fałszywych budżetów ms per model).
### Dysk vs wątek główny
| Etap | Wątek | API |
|------|-------|-----|
| Deserialize MODL z `.eu7` | Worker | `read_pack_section()` |
| Page cache `.e3d` | Worker | `prefetch_pack_models()` |
| `GetModel()` unikalnych meshów + instancje | **Main** | `insert_eu7_pack_models()` |
| GPU draw | **Main** | `Render_Instanced()` dla `m_instanceable` |
`TModel3d::LoadFromFile()` + `GfxRenderer`**tylko main**. W sekcji: najpierw wszystkie unikalne meshe, potem tysiące lekkich `TAnimModel` trafiających do bucketów instancingu.
### Przepływ ładowania
```
1. read_module() → PIDX w RAM, PACK jako offset (seek)
2. init_section_stream() → worker pool
3. preload_section_stream() [loader, blokująco] → bootstrap + drain sekcji
4. W jazdzie (is_ready):
kick_section_stream_bootstrap() — enqueue, bez spin-wait
drain_section_stream() — jedna gotowa sekcja → apply_pending_section()
update_section_stream(camera)
```
### Wątki robocze
Po `read_pack_section()`: `prefetch_pack_models()``PackSectionReady` w kolejce.
### Drain (wątek główny)
`apply_pending_section()`**cała sekcja** w jednym `insert_eu7_pack_models()`:
1. `preload_unique_pack_meshes` — cold load tylko dla nowych `.e3d`
2. Pętla instancji → `Region->insert` → GPU instancing bucket
Jedna sekcja na wywołanie `drain_section_stream()` (jedna klatka = jedna sekcja, nie jeden model).
### Diagnostyka (`eu7_load_stats`)
| Pole | Znaczenie |
|------|-----------|
| `model_ms` / `models` | Czas i liczba `TAnimModel` z PACK |
| `pack_sections_loaded` / `pack_models` | Sekcje / instancje ze streamingu |
Overlay renderera: `inst-pool:` — ile instancji zakwalifikowanych do `Render_Instanced`.
### Pliki implementacji (runtime)
| Temat | Plik |
|-------|------|
| Streaming sekcji | `scene/eu7/eu7_section_stream.cpp` |
| Prefetch dysku | `scene/eu7/eu7_model_prefetch.cpp` |
| Instancje PACK | `simulation/simulationstateserializer.cpp` |
| `LoadEu7` + `m_instanceable` | `model/AnimModel.cpp` |
| `Render_Instanced` | `rendering/opengl33renderer.cpp` |
| Cache meshów | `model/MdlMngr.cpp` |
---
## 32. Historia wersji
| Wersja | Zmiany |
|--------|--------------------------------------------------------------------------------------------------|
| v1 | Format legacy — szczegóły nieokreślone, nieobsługiwany |
| v2 | Skalary i Vec3 jako float64 (8 B) na dysku |
| v3 | Skalary i Vec3 jako float32 (4 B) na dysku — `writeF64()` → rzutuje do float32 |
| v4 | **Slim node** (flagowany nagłówek węzła) + **packed vertex** (snorm16 normy + half16 UV) |
| v5 | Chunk **TERR** z trybem batched (terrain per sekcja 1 km); TERR może być w osobnym pliku `.eu7` |
| v6 | Chunk INCL rozszerzony o **site transform** (pełny TransformContext per include) |
| v7 | Chunki **PIDX + PACK** — modele scenerii podzielone na kafelki 1 km, asynchroniczny streaming |
| v8 | Chunk **PROT** (prototypy) + nowy format sekcji PACK z `solo_count` + `inst_count` |
---
## 33. Ścieżki plików i konwencje nazewnictwa
### Rozszerzenie
Pliki EU7B używają rozszerzenia `.eu7`. Identyfikacja pliku odbywa się przez:
1. Sprawdzenie rozszerzenia (`isSceneBinaryPath()`)
2. Sondowanie magic bytes `EU7B` na pozycji 0 (`probeSceneBinaryMagic()`)
### Konwersja ścieżek source → binary
- `*.scm``*.eu7` (te same katalog i stem)
- `*.inc``*.eu7`
- `*.scn``*.eu7`
- `*.sbt` → terrain `*.eu7` (odpowiednik binarny terenu)
Funkcja `binary_path()` w loaderze zamienia rozszerzenie przy tym samym stemie.
### Warianty pliku terenu
Plik `.eu7` może zawierać chunk TERR (teren). Loader sprawdza obecność TERR przez `probe_terrain_file()` (pełne wczytanie nagłówków).
Funkcja `is_scenario_terrain()` sprawdza czy obok scenariusza `<stem>.scn` istnieje `<stem>.eu7` z chunkiem TERR.
### Pliki include
Submoduły include są wczytywane jeden raz per ścieżka (deduplication przez `is_module_loaded()`). Ścieżka pliku `.eu7` includowanego jest wyznaczana przez `include_eu7_path()` — ten sam katalog co plik SCM/INC.
---
*Dokumentacja wygenerowana na podstawie kodu źródłowego; stan na 11 czerwca 2026.*

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

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