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:
@@ -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})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ); } }
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 { ; }
|
||||
|
||||
@@ -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
951
docs/eu7-format.md
Normal 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 v4–v8.
|
||||
|
||||
---
|
||||
|
||||
## 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 4–8.** Wersje 1–3 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 — 1–18 = 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.*
|
||||
117
eu07-parser/include/eu07/geo/puwg1992.hpp
Normal file
117
eu07-parser/include/eu07/geo/puwg1992.hpp
Normal 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
|
||||
86
eu07-parser/include/eu07/geo/spatial_grid.hpp
Normal file
86
eu07-parser/include/eu07/geo/spatial_grid.hpp
Normal 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
|
||||
323
eu07-parser/include/eu07/nmt/asc_index.hpp
Normal file
323
eu07-parser/include/eu07/nmt/asc_index.hpp
Normal 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
|
||||
152
eu07-parser/include/eu07/nmt/asc_lookup.hpp
Normal file
152
eu07-parser/include/eu07/nmt/asc_lookup.hpp
Normal 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
|
||||
228
eu07-parser/include/eu07/nmt/asc_ram.hpp
Normal file
228
eu07-parser/include/eu07/nmt/asc_ram.hpp
Normal 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
|
||||
16
eu07-parser/include/eu07/nmt/parallel.hpp
Normal file
16
eu07-parser/include/eu07/nmt/parallel.hpp
Normal 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
|
||||
92
eu07-parser/include/eu07/nmt/profile_export.hpp
Normal file
92
eu07-parser/include/eu07/nmt/profile_export.hpp
Normal 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
|
||||
14
eu07-parser/include/eu07/nmt/profile_height.hpp
Normal file
14
eu07-parser/include/eu07/nmt/profile_height.hpp
Normal 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
|
||||
102
eu07-parser/include/eu07/nmt/profile_slope.hpp
Normal file
102
eu07-parser/include/eu07/nmt/profile_slope.hpp
Normal 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
|
||||
25
eu07-parser/include/eu07/nmt/profile_types.hpp
Normal file
25
eu07-parser/include/eu07/nmt/profile_types.hpp
Normal 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
|
||||
52
eu07-parser/include/eu07/nmt/progress.hpp
Normal file
52
eu07-parser/include/eu07/nmt/progress.hpp
Normal 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
|
||||
462
eu07-parser/include/eu07/nmt/route_geometry.hpp
Normal file
462
eu07-parser/include/eu07/nmt/route_geometry.hpp
Normal 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
|
||||
14
eu07-parser/include/eu07/nmt/terrain.hpp
Normal file
14
eu07-parser/include/eu07/nmt/terrain.hpp
Normal 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>
|
||||
33
eu07-parser/include/eu07/nmt/terrain_cli.hpp
Normal file
33
eu07-parser/include/eu07/nmt/terrain_cli.hpp
Normal 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
|
||||
102
eu07-parser/include/eu07/nmt/terrain_pipeline.hpp
Normal file
102
eu07-parser/include/eu07/nmt/terrain_pipeline.hpp
Normal 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
|
||||
500
eu07-parser/include/eu07/parser.hpp
Normal file
500
eu07-parser/include/eu07/parser.hpp
Normal 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
|
||||
46
eu07-parser/include/eu07/scene/area.hpp
Normal file
46
eu07-parser/include/eu07/scene/area.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/atmo.hpp
Normal file
35
eu07-parser/include/eu07/scene/atmo.hpp
Normal 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
|
||||
807
eu07-parser/include/eu07/scene/bake/bake_tree.hpp
Normal file
807
eu07-parser/include/eu07/scene/bake/bake_tree.hpp
Normal 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
|
||||
|
||||
|
||||
1222
eu07-parser/include/eu07/scene/bake/compose_pack_models.hpp
Normal file
1222
eu07-parser/include/eu07/scene/bake/compose_pack_models.hpp
Normal file
File diff suppressed because it is too large
Load Diff
33
eu07-parser/include/eu07/scene/bake/dynamic.hpp
Normal file
33
eu07-parser/include/eu07/scene/bake/dynamic.hpp
Normal 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
|
||||
164
eu07-parser/include/eu07/scene/bake/event.hpp
Normal file
164
eu07-parser/include/eu07/scene/bake/event.hpp
Normal 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
|
||||
59
eu07-parser/include/eu07/scene/bake/eventlauncher.hpp
Normal file
59
eu07-parser/include/eu07/scene/bake/eventlauncher.hpp
Normal 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
|
||||
188
eu07-parser/include/eu07/scene/bake/geometry.hpp
Normal file
188
eu07-parser/include/eu07/scene/bake/geometry.hpp
Normal 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
|
||||
82
eu07-parser/include/eu07/scene/bake/lines.hpp
Normal file
82
eu07-parser/include/eu07/scene/bake/lines.hpp
Normal 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
|
||||
24
eu07-parser/include/eu07/scene/bake/memcell.hpp
Normal file
24
eu07-parser/include/eu07/scene/bake/memcell.hpp
Normal 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
|
||||
69
eu07-parser/include/eu07/scene/bake/mesh.hpp
Normal file
69
eu07-parser/include/eu07/scene/bake/mesh.hpp
Normal 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
|
||||
64
eu07-parser/include/eu07/scene/bake/model.hpp
Normal file
64
eu07-parser/include/eu07/scene/bake/model.hpp
Normal 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
|
||||
164
eu07-parser/include/eu07/scene/bake/module.hpp
Normal file
164
eu07-parser/include/eu07/scene/bake/module.hpp
Normal 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
|
||||
22
eu07-parser/include/eu07/scene/bake/sound.hpp
Normal file
22
eu07-parser/include/eu07/scene/bake/sound.hpp
Normal 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
|
||||
330
eu07-parser/include/eu07/scene/bake/track.hpp
Normal file
330
eu07-parser/include/eu07/scene/bake/track.hpp
Normal 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
|
||||
64
eu07-parser/include/eu07/scene/bake/traction.hpp
Normal file
64
eu07-parser/include/eu07/scene/bake/traction.hpp
Normal 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
|
||||
48
eu07-parser/include/eu07/scene/bake/traction_power.hpp
Normal file
48
eu07-parser/include/eu07/scene/bake/traction_power.hpp
Normal 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
|
||||
77
eu07-parser/include/eu07/scene/bake/trainset.hpp
Normal file
77
eu07-parser/include/eu07/scene/bake/trainset.hpp
Normal 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
|
||||
272
eu07-parser/include/eu07/scene/binary/codec.hpp
Normal file
272
eu07-parser/include/eu07/scene/binary/codec.hpp
Normal 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
|
||||
119
eu07-parser/include/eu07/scene/binary/common.hpp
Normal file
119
eu07-parser/include/eu07/scene/binary/common.hpp
Normal 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
|
||||
717
eu07-parser/include/eu07/scene/binary/detokenize.hpp
Normal file
717
eu07-parser/include/eu07/scene/binary/detokenize.hpp
Normal 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
|
||||
141
eu07-parser/include/eu07/scene/binary/io.hpp
Normal file
141
eu07-parser/include/eu07/scene/binary/io.hpp
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
429
eu07-parser/include/eu07/scene/binary/runtime_chunks_sim.hpp
Normal file
429
eu07-parser/include/eu07/scene/binary/runtime_chunks_sim.hpp
Normal 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));
|
||||
}
|
||||
}
|
||||
737
eu07-parser/include/eu07/scene/binary/runtime_codec.hpp
Normal file
737
eu07-parser/include/eu07/scene/binary/runtime_codec.hpp
Normal 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
|
||||
1058
eu07-parser/include/eu07/scene/binary/runtime_module.hpp
Normal file
1058
eu07-parser/include/eu07/scene/binary/runtime_module.hpp
Normal file
File diff suppressed because it is too large
Load Diff
133
eu07-parser/include/eu07/scene/binary/selftest.hpp
Normal file
133
eu07-parser/include/eu07/scene/binary/selftest.hpp
Normal 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
|
||||
75
eu07-parser/include/eu07/scene/binary/string_table.hpp
Normal file
75
eu07-parser/include/eu07/scene/binary/string_table.hpp
Normal 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
|
||||
112
eu07-parser/include/eu07/scene/boundaries.hpp
Normal file
112
eu07-parser/include/eu07/scene/boundaries.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/camera.hpp
Normal file
35
eu07-parser/include/eu07/scene/camera.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/config.hpp
Normal file
35
eu07-parser/include/eu07/scene/config.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/context.hpp
Normal file
35
eu07-parser/include/eu07/scene/context.hpp
Normal 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
|
||||
58
eu07-parser/include/eu07/scene/cursor.hpp
Normal file
58
eu07-parser/include/eu07/scene/cursor.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/description.hpp
Normal file
35
eu07-parser/include/eu07/scene/description.hpp
Normal 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
|
||||
25
eu07-parser/include/eu07/scene/detail/boundary.hpp
Normal file
25
eu07-parser/include/eu07/scene/detail/boundary.hpp
Normal 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
|
||||
70
eu07-parser/include/eu07/scene/detail/parse.hpp
Normal file
70
eu07-parser/include/eu07/scene/detail/parse.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/detail/subtype.hpp
Normal file
35
eu07-parser/include/eu07/scene/detail/subtype.hpp
Normal 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
|
||||
110
eu07-parser/include/eu07/scene/dispatch_table.hpp
Normal file
110
eu07-parser/include/eu07/scene/dispatch_table.hpp
Normal 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
|
||||
109
eu07-parser/include/eu07/scene/document.hpp
Normal file
109
eu07-parser/include/eu07/scene/document.hpp
Normal 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
|
||||
37
eu07-parser/include/eu07/scene/event.hpp
Normal file
37
eu07-parser/include/eu07/scene/event.hpp
Normal 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
|
||||
23
eu07-parser/include/eu07/scene/first_init.hpp
Normal file
23
eu07-parser/include/eu07/scene/first_init.hpp
Normal 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
|
||||
24
eu07-parser/include/eu07/scene/group.hpp
Normal file
24
eu07-parser/include/eu07/scene/group.hpp
Normal 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
|
||||
73
eu07-parser/include/eu07/scene/include.hpp
Normal file
73
eu07-parser/include/eu07/scene/include.hpp
Normal 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
|
||||
66
eu07-parser/include/eu07/scene/include_placement.hpp
Normal file
66
eu07-parser/include/eu07/scene/include_placement.hpp
Normal 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
|
||||
619
eu07-parser/include/eu07/scene/include_resolve.hpp
Normal file
619
eu07-parser/include/eu07/scene/include_resolve.hpp
Normal 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
|
||||
|
||||
|
||||
47
eu07-parser/include/eu07/scene/include_scan.hpp
Normal file
47
eu07-parser/include/eu07/scene/include_scan.hpp
Normal 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
|
||||
23
eu07-parser/include/eu07/scene/include_types.hpp
Normal file
23
eu07-parser/include/eu07/scene/include_types.hpp
Normal 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
|
||||
48
eu07-parser/include/eu07/scene/isolated.hpp
Normal file
48
eu07-parser/include/eu07/scene/isolated.hpp
Normal 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
|
||||
57
eu07-parser/include/eu07/scene/keywords.hpp
Normal file
57
eu07-parser/include/eu07/scene/keywords.hpp
Normal 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
|
||||
35
eu07-parser/include/eu07/scene/light.hpp
Normal file
35
eu07-parser/include/eu07/scene/light.hpp
Normal 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
|
||||
31
eu07-parser/include/eu07/scene/lua.hpp
Normal file
31
eu07-parser/include/eu07/scene/lua.hpp
Normal 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
|
||||
33
eu07-parser/include/eu07/scene/match.hpp
Normal file
33
eu07-parser/include/eu07/scene/match.hpp
Normal 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
|
||||
66
eu07-parser/include/eu07/scene/node.hpp
Normal file
66
eu07-parser/include/eu07/scene/node.hpp
Normal 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
|
||||
60
eu07-parser/include/eu07/scene/node/common.hpp
Normal file
60
eu07-parser/include/eu07/scene/node/common.hpp
Normal 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
|
||||
128
eu07-parser/include/eu07/scene/node/dynamic.hpp
Normal file
128
eu07-parser/include/eu07/scene/node/dynamic.hpp
Normal 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
|
||||
169
eu07-parser/include/eu07/scene/node/eventlauncher.hpp
Normal file
169
eu07-parser/include/eu07/scene/node/eventlauncher.hpp
Normal 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
|
||||
31
eu07-parser/include/eu07/scene/node/header.hpp
Normal file
31
eu07-parser/include/eu07/scene/node/header.hpp
Normal 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
|
||||
295
eu07-parser/include/eu07/scene/node/kinds.hpp
Normal file
295
eu07-parser/include/eu07/scene/node/kinds.hpp
Normal 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
|
||||
53
eu07-parser/include/eu07/scene/node/line_loop.hpp
Normal file
53
eu07-parser/include/eu07/scene/node/line_loop.hpp
Normal 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
|
||||
53
eu07-parser/include/eu07/scene/node/line_strip.hpp
Normal file
53
eu07-parser/include/eu07/scene/node/line_strip.hpp
Normal 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
|
||||
61
eu07-parser/include/eu07/scene/node/lines.hpp
Normal file
61
eu07-parser/include/eu07/scene/node/lines.hpp
Normal 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
|
||||
56
eu07-parser/include/eu07/scene/node/memcell.hpp
Normal file
56
eu07-parser/include/eu07/scene/node/memcell.hpp
Normal 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
|
||||
45
eu07-parser/include/eu07/scene/node/mesh.hpp
Normal file
45
eu07-parser/include/eu07/scene/node/mesh.hpp
Normal 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
|
||||
175
eu07-parser/include/eu07/scene/node/model.hpp
Normal file
175
eu07-parser/include/eu07/scene/node/model.hpp
Normal 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
|
||||
165
eu07-parser/include/eu07/scene/node/parse_util.hpp
Normal file
165
eu07-parser/include/eu07/scene/node/parse_util.hpp
Normal 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
|
||||
74
eu07-parser/include/eu07/scene/node/shared.hpp
Normal file
74
eu07-parser/include/eu07/scene/node/shared.hpp
Normal 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
|
||||
40
eu07-parser/include/eu07/scene/node/sound.hpp
Normal file
40
eu07-parser/include/eu07/scene/node/sound.hpp
Normal 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
|
||||
736
eu07-parser/include/eu07/scene/node/track.hpp
Normal file
736
eu07-parser/include/eu07/scene/node/track.hpp
Normal 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
|
||||
100
eu07-parser/include/eu07/scene/node/traction.hpp
Normal file
100
eu07-parser/include/eu07/scene/node/traction.hpp
Normal 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
|
||||
80
eu07-parser/include/eu07/scene/node/traction_power.hpp
Normal file
80
eu07-parser/include/eu07/scene/node/traction_power.hpp
Normal 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
|
||||
36
eu07-parser/include/eu07/scene/node/triangle_fan.hpp
Normal file
36
eu07-parser/include/eu07/scene/node/triangle_fan.hpp
Normal 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
|
||||
36
eu07-parser/include/eu07/scene/node/triangle_strip.hpp
Normal file
36
eu07-parser/include/eu07/scene/node/triangle_strip.hpp
Normal 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
|
||||
122
eu07-parser/include/eu07/scene/node/triangles.hpp
Normal file
122
eu07-parser/include/eu07/scene/node/triangles.hpp
Normal 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
|
||||
38
eu07-parser/include/eu07/scene/node/types.hpp
Normal file
38
eu07-parser/include/eu07/scene/node/types.hpp
Normal 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
|
||||
40
eu07-parser/include/eu07/scene/origin.hpp
Normal file
40
eu07-parser/include/eu07/scene/origin.hpp
Normal 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
Reference in New Issue
Block a user