From 876a4c7c187e6d262107bcac6e0b29e155f837db Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 13 Jun 2026 15:43:44 +0200 Subject: [PATCH] EU7 scenery format and models streaming --- CMakeLists.txt | 40 +- application/application.cpp | 59 + application/application.h | 11 + application/applicationmode.h | 2 +- application/drivermode.cpp | 41 +- application/scenarioloadermode.cpp | 100 +- application/scenarioloadermode.h | 14 + application/scenarioloaderuilayer.cpp | 13 +- docs/eu7-format.md | 951 ++++++++ eu07-parser/include/eu07/geo/puwg1992.hpp | 117 + eu07-parser/include/eu07/geo/spatial_grid.hpp | 86 + eu07-parser/include/eu07/nmt/asc_index.hpp | 323 +++ eu07-parser/include/eu07/nmt/asc_lookup.hpp | 152 ++ eu07-parser/include/eu07/nmt/asc_ram.hpp | 228 ++ eu07-parser/include/eu07/nmt/parallel.hpp | 16 + .../include/eu07/nmt/profile_export.hpp | 92 + .../include/eu07/nmt/profile_height.hpp | 14 + .../include/eu07/nmt/profile_slope.hpp | 102 + .../include/eu07/nmt/profile_types.hpp | 25 + eu07-parser/include/eu07/nmt/progress.hpp | 52 + .../include/eu07/nmt/route_geometry.hpp | 462 ++++ eu07-parser/include/eu07/nmt/terrain.hpp | 14 + eu07-parser/include/eu07/nmt/terrain_cli.hpp | 33 + .../include/eu07/nmt/terrain_pipeline.hpp | 102 + eu07-parser/include/eu07/parser.hpp | 500 ++++ eu07-parser/include/eu07/scene/area.hpp | 46 + eu07-parser/include/eu07/scene/atmo.hpp | 35 + .../include/eu07/scene/bake/bake_tree.hpp | 807 +++++++ .../eu07/scene/bake/compose_pack_models.hpp | 1222 ++++++++++ .../include/eu07/scene/bake/dynamic.hpp | 33 + eu07-parser/include/eu07/scene/bake/event.hpp | 164 ++ .../include/eu07/scene/bake/eventlauncher.hpp | 59 + .../include/eu07/scene/bake/geometry.hpp | 188 ++ eu07-parser/include/eu07/scene/bake/lines.hpp | 82 + .../include/eu07/scene/bake/memcell.hpp | 24 + eu07-parser/include/eu07/scene/bake/mesh.hpp | 69 + eu07-parser/include/eu07/scene/bake/model.hpp | 64 + .../include/eu07/scene/bake/module.hpp | 164 ++ eu07-parser/include/eu07/scene/bake/sound.hpp | 22 + eu07-parser/include/eu07/scene/bake/track.hpp | 330 +++ .../include/eu07/scene/bake/traction.hpp | 64 + .../eu07/scene/bake/traction_power.hpp | 48 + .../include/eu07/scene/bake/trainset.hpp | 77 + .../include/eu07/scene/binary/codec.hpp | 272 +++ .../include/eu07/scene/binary/common.hpp | 119 + .../include/eu07/scene/binary/detokenize.hpp | 717 ++++++ eu07-parser/include/eu07/scene/binary/io.hpp | 141 ++ .../binary/runtime_chunks_directives.hpp | 165 ++ .../eu07/scene/binary/runtime_chunks_sim.hpp | 429 ++++ .../eu07/scene/binary/runtime_codec.hpp | 737 ++++++ .../eu07/scene/binary/runtime_module.hpp | 1058 +++++++++ .../include/eu07/scene/binary/selftest.hpp | 133 ++ .../eu07/scene/binary/string_table.hpp | 75 + eu07-parser/include/eu07/scene/boundaries.hpp | 112 + eu07-parser/include/eu07/scene/camera.hpp | 35 + eu07-parser/include/eu07/scene/config.hpp | 35 + eu07-parser/include/eu07/scene/context.hpp | 35 + eu07-parser/include/eu07/scene/cursor.hpp | 58 + .../include/eu07/scene/description.hpp | 35 + .../include/eu07/scene/detail/boundary.hpp | 25 + .../include/eu07/scene/detail/parse.hpp | 70 + .../include/eu07/scene/detail/subtype.hpp | 35 + .../include/eu07/scene/dispatch_table.hpp | 110 + eu07-parser/include/eu07/scene/document.hpp | 109 + eu07-parser/include/eu07/scene/event.hpp | 37 + eu07-parser/include/eu07/scene/first_init.hpp | 23 + eu07-parser/include/eu07/scene/group.hpp | 24 + eu07-parser/include/eu07/scene/include.hpp | 73 + .../include/eu07/scene/include_placement.hpp | 66 + .../include/eu07/scene/include_resolve.hpp | 619 +++++ .../include/eu07/scene/include_scan.hpp | 47 + .../include/eu07/scene/include_types.hpp | 23 + eu07-parser/include/eu07/scene/isolated.hpp | 48 + eu07-parser/include/eu07/scene/keywords.hpp | 57 + eu07-parser/include/eu07/scene/light.hpp | 35 + eu07-parser/include/eu07/scene/lua.hpp | 31 + eu07-parser/include/eu07/scene/match.hpp | 33 + eu07-parser/include/eu07/scene/node.hpp | 66 + .../include/eu07/scene/node/common.hpp | 60 + .../include/eu07/scene/node/dynamic.hpp | 128 ++ .../include/eu07/scene/node/eventlauncher.hpp | 169 ++ .../include/eu07/scene/node/header.hpp | 31 + eu07-parser/include/eu07/scene/node/kinds.hpp | 295 +++ .../include/eu07/scene/node/line_loop.hpp | 53 + .../include/eu07/scene/node/line_strip.hpp | 53 + eu07-parser/include/eu07/scene/node/lines.hpp | 61 + .../include/eu07/scene/node/memcell.hpp | 56 + eu07-parser/include/eu07/scene/node/mesh.hpp | 45 + eu07-parser/include/eu07/scene/node/model.hpp | 175 ++ .../include/eu07/scene/node/parse_util.hpp | 165 ++ .../include/eu07/scene/node/shared.hpp | 74 + eu07-parser/include/eu07/scene/node/sound.hpp | 40 + eu07-parser/include/eu07/scene/node/track.hpp | 736 ++++++ .../include/eu07/scene/node/traction.hpp | 100 + .../eu07/scene/node/traction_power.hpp | 80 + .../include/eu07/scene/node/triangle_fan.hpp | 36 + .../eu07/scene/node/triangle_strip.hpp | 36 + .../include/eu07/scene/node/triangles.hpp | 122 + eu07-parser/include/eu07/scene/node/types.hpp | 38 + eu07-parser/include/eu07/scene/origin.hpp | 40 + eu07-parser/include/eu07/scene/processor.hpp | 243 ++ eu07-parser/include/eu07/scene/report.hpp | 202 ++ eu07-parser/include/eu07/scene/rotate.hpp | 36 + eu07-parser/include/eu07/scene/runtime.hpp | 23 + .../include/eu07/scene/runtime/basic_node.hpp | 45 + .../include/eu07/scene/runtime/directives.hpp | 77 + .../include/eu07/scene/runtime/nodes.hpp | 220 ++ .../include/eu07/scene/runtime/scene.hpp | 53 + .../include/eu07/scene/runtime/scratch.hpp | 38 + .../include/eu07/scene/runtime/transform.hpp | 213 ++ .../include/eu07/scene/runtime/types.hpp | 65 + eu07-parser/include/eu07/scene/scale.hpp | 36 + eu07-parser/include/eu07/scene/scratch.hpp | 109 + eu07-parser/include/eu07/scene/sky.hpp | 34 + eu07-parser/include/eu07/scene/terrain.hpp | 34 + eu07-parser/include/eu07/scene/test.hpp | 32 + eu07-parser/include/eu07/scene/time.hpp | 35 + .../include/eu07/scene/track_routes.hpp | 608 +++++ eu07-parser/include/eu07/scene/trainset.hpp | 50 + eu07-parser/include/eu07/scene/wiki.hpp | 53 + model/AnimModel.cpp | 274 ++- model/AnimModel.h | 28 + model/MdlMngr.cpp | 9 + model/MdlMngr.h | 1 + model/Model3d.cpp | 17 +- rendering/opengl33renderer.cpp | 23 +- rendering/openglrenderer.cpp | 8 +- scene/eu7/eu7_apply.cpp | 194 ++ scene/eu7/eu7_apply.h | 37 + scene/eu7/eu7_bake.cpp | 337 +++ scene/eu7/eu7_bake.h | 30 + scene/eu7/eu7_bake_parser.cpp | 170 ++ scene/eu7/eu7_bake_parser.h | 22 + scene/eu7/eu7_chunks.h | 62 + scene/eu7/eu7_emit.cpp | 790 +++++++ scene/eu7/eu7_emit.h | 34 + scene/eu7/eu7_load_stats.cpp | 148 ++ scene/eu7/eu7_load_stats.h | 85 + scene/eu7/eu7_loader.cpp | 587 +++++ scene/eu7/eu7_loader.h | 123 + scene/eu7/eu7_model_prefetch.cpp | 135 ++ scene/eu7/eu7_model_prefetch.h | 33 + scene/eu7/eu7_pack_bench.cpp | 405 ++++ scene/eu7/eu7_pack_bench.h | 159 ++ scene/eu7/eu7_parameters.cpp | 282 +++ scene/eu7/eu7_parameters.h | 51 + scene/eu7/eu7_reader.cpp | 1149 ++++++++++ scene/eu7/eu7_reader.h | 61 + scene/eu7/eu7_section.cpp | 47 + scene/eu7/eu7_section.h | 33 + scene/eu7/eu7_section_stream.cpp | 2042 +++++++++++++++++ scene/eu7/eu7_section_stream.h | 105 + scene/eu7/eu7_transform.h | 258 +++ scene/eu7/eu7_types.h | 393 ++++ scene/scene.cpp | 15 + scene/scenenode.h | 12 + simulation/simulation.cpp | 13 +- simulation/simulation.h | 7 +- simulation/simulationstateserializer.cpp | 1591 ++++++++++++- simulation/simulationstateserializer.h | 95 + tools/parse_minidump.py | 174 ++ utilities/Globals.cpp | 21 + utilities/Globals.h | 3 + utilities/parser.cpp | 46 +- vehicle/DynObj.cpp | 80 +- vehicle/DynObj.h | 4 + world/EvLaunch.cpp | 3 +- world/Event.cpp | 78 + world/Event.h | 4 + world/Track.cpp | 387 ++-- world/Track.h | 7 + 171 files changed, 27924 insertions(+), 281 deletions(-) create mode 100644 docs/eu7-format.md create mode 100644 eu07-parser/include/eu07/geo/puwg1992.hpp create mode 100644 eu07-parser/include/eu07/geo/spatial_grid.hpp create mode 100644 eu07-parser/include/eu07/nmt/asc_index.hpp create mode 100644 eu07-parser/include/eu07/nmt/asc_lookup.hpp create mode 100644 eu07-parser/include/eu07/nmt/asc_ram.hpp create mode 100644 eu07-parser/include/eu07/nmt/parallel.hpp create mode 100644 eu07-parser/include/eu07/nmt/profile_export.hpp create mode 100644 eu07-parser/include/eu07/nmt/profile_height.hpp create mode 100644 eu07-parser/include/eu07/nmt/profile_slope.hpp create mode 100644 eu07-parser/include/eu07/nmt/profile_types.hpp create mode 100644 eu07-parser/include/eu07/nmt/progress.hpp create mode 100644 eu07-parser/include/eu07/nmt/route_geometry.hpp create mode 100644 eu07-parser/include/eu07/nmt/terrain.hpp create mode 100644 eu07-parser/include/eu07/nmt/terrain_cli.hpp create mode 100644 eu07-parser/include/eu07/nmt/terrain_pipeline.hpp create mode 100644 eu07-parser/include/eu07/parser.hpp create mode 100644 eu07-parser/include/eu07/scene/area.hpp create mode 100644 eu07-parser/include/eu07/scene/atmo.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/bake_tree.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/compose_pack_models.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/dynamic.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/event.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/eventlauncher.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/geometry.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/lines.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/memcell.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/mesh.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/model.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/module.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/sound.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/track.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/traction.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/traction_power.hpp create mode 100644 eu07-parser/include/eu07/scene/bake/trainset.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/codec.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/common.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/detokenize.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/io.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/runtime_chunks_directives.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/runtime_chunks_sim.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/runtime_codec.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/runtime_module.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/selftest.hpp create mode 100644 eu07-parser/include/eu07/scene/binary/string_table.hpp create mode 100644 eu07-parser/include/eu07/scene/boundaries.hpp create mode 100644 eu07-parser/include/eu07/scene/camera.hpp create mode 100644 eu07-parser/include/eu07/scene/config.hpp create mode 100644 eu07-parser/include/eu07/scene/context.hpp create mode 100644 eu07-parser/include/eu07/scene/cursor.hpp create mode 100644 eu07-parser/include/eu07/scene/description.hpp create mode 100644 eu07-parser/include/eu07/scene/detail/boundary.hpp create mode 100644 eu07-parser/include/eu07/scene/detail/parse.hpp create mode 100644 eu07-parser/include/eu07/scene/detail/subtype.hpp create mode 100644 eu07-parser/include/eu07/scene/dispatch_table.hpp create mode 100644 eu07-parser/include/eu07/scene/document.hpp create mode 100644 eu07-parser/include/eu07/scene/event.hpp create mode 100644 eu07-parser/include/eu07/scene/first_init.hpp create mode 100644 eu07-parser/include/eu07/scene/group.hpp create mode 100644 eu07-parser/include/eu07/scene/include.hpp create mode 100644 eu07-parser/include/eu07/scene/include_placement.hpp create mode 100644 eu07-parser/include/eu07/scene/include_resolve.hpp create mode 100644 eu07-parser/include/eu07/scene/include_scan.hpp create mode 100644 eu07-parser/include/eu07/scene/include_types.hpp create mode 100644 eu07-parser/include/eu07/scene/isolated.hpp create mode 100644 eu07-parser/include/eu07/scene/keywords.hpp create mode 100644 eu07-parser/include/eu07/scene/light.hpp create mode 100644 eu07-parser/include/eu07/scene/lua.hpp create mode 100644 eu07-parser/include/eu07/scene/match.hpp create mode 100644 eu07-parser/include/eu07/scene/node.hpp create mode 100644 eu07-parser/include/eu07/scene/node/common.hpp create mode 100644 eu07-parser/include/eu07/scene/node/dynamic.hpp create mode 100644 eu07-parser/include/eu07/scene/node/eventlauncher.hpp create mode 100644 eu07-parser/include/eu07/scene/node/header.hpp create mode 100644 eu07-parser/include/eu07/scene/node/kinds.hpp create mode 100644 eu07-parser/include/eu07/scene/node/line_loop.hpp create mode 100644 eu07-parser/include/eu07/scene/node/line_strip.hpp create mode 100644 eu07-parser/include/eu07/scene/node/lines.hpp create mode 100644 eu07-parser/include/eu07/scene/node/memcell.hpp create mode 100644 eu07-parser/include/eu07/scene/node/mesh.hpp create mode 100644 eu07-parser/include/eu07/scene/node/model.hpp create mode 100644 eu07-parser/include/eu07/scene/node/parse_util.hpp create mode 100644 eu07-parser/include/eu07/scene/node/shared.hpp create mode 100644 eu07-parser/include/eu07/scene/node/sound.hpp create mode 100644 eu07-parser/include/eu07/scene/node/track.hpp create mode 100644 eu07-parser/include/eu07/scene/node/traction.hpp create mode 100644 eu07-parser/include/eu07/scene/node/traction_power.hpp create mode 100644 eu07-parser/include/eu07/scene/node/triangle_fan.hpp create mode 100644 eu07-parser/include/eu07/scene/node/triangle_strip.hpp create mode 100644 eu07-parser/include/eu07/scene/node/triangles.hpp create mode 100644 eu07-parser/include/eu07/scene/node/types.hpp create mode 100644 eu07-parser/include/eu07/scene/origin.hpp create mode 100644 eu07-parser/include/eu07/scene/processor.hpp create mode 100644 eu07-parser/include/eu07/scene/report.hpp create mode 100644 eu07-parser/include/eu07/scene/rotate.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/basic_node.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/directives.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/nodes.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/scene.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/scratch.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/transform.hpp create mode 100644 eu07-parser/include/eu07/scene/runtime/types.hpp create mode 100644 eu07-parser/include/eu07/scene/scale.hpp create mode 100644 eu07-parser/include/eu07/scene/scratch.hpp create mode 100644 eu07-parser/include/eu07/scene/sky.hpp create mode 100644 eu07-parser/include/eu07/scene/terrain.hpp create mode 100644 eu07-parser/include/eu07/scene/test.hpp create mode 100644 eu07-parser/include/eu07/scene/time.hpp create mode 100644 eu07-parser/include/eu07/scene/track_routes.hpp create mode 100644 eu07-parser/include/eu07/scene/trainset.hpp create mode 100644 eu07-parser/include/eu07/scene/wiki.hpp create mode 100644 scene/eu7/eu7_apply.cpp create mode 100644 scene/eu7/eu7_apply.h create mode 100644 scene/eu7/eu7_bake.cpp create mode 100644 scene/eu7/eu7_bake.h create mode 100644 scene/eu7/eu7_bake_parser.cpp create mode 100644 scene/eu7/eu7_bake_parser.h create mode 100644 scene/eu7/eu7_chunks.h create mode 100644 scene/eu7/eu7_emit.cpp create mode 100644 scene/eu7/eu7_emit.h create mode 100644 scene/eu7/eu7_load_stats.cpp create mode 100644 scene/eu7/eu7_load_stats.h create mode 100644 scene/eu7/eu7_loader.cpp create mode 100644 scene/eu7/eu7_loader.h create mode 100644 scene/eu7/eu7_model_prefetch.cpp create mode 100644 scene/eu7/eu7_model_prefetch.h create mode 100644 scene/eu7/eu7_pack_bench.cpp create mode 100644 scene/eu7/eu7_pack_bench.h create mode 100644 scene/eu7/eu7_parameters.cpp create mode 100644 scene/eu7/eu7_parameters.h create mode 100644 scene/eu7/eu7_reader.cpp create mode 100644 scene/eu7/eu7_reader.h create mode 100644 scene/eu7/eu7_section.cpp create mode 100644 scene/eu7/eu7_section.h create mode 100644 scene/eu7/eu7_section_stream.cpp create mode 100644 scene/eu7/eu7_section_stream.h create mode 100644 scene/eu7/eu7_transform.h create mode 100644 scene/eu7/eu7_types.h create mode 100644 tools/parse_minidump.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 85007284..705b416e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 "$<$:${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}) diff --git a/application/application.cpp b/application/application.cpp index ba09bde9..4ed3e8e8 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -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(); + } +} + +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(); + } + + 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(); + } + 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; diff --git a/application/application.h b/application/application.h index 11415603..c678822d 100644 --- a/application/application.h +++ b/application/application.h @@ -9,6 +9,8 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include + #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 m_loading_overlay; modeptr_array m_modes { nullptr }; // collection of available application behaviour modes mode_stack m_modestack; // current behaviour mode diff --git a/application/applicationmode.h b/application/applicationmode.h index feea3bdb..75f70362 100644 --- a/application/applicationmode.h +++ b/application/applicationmode.h @@ -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 ); } } diff --git a/application/drivermode.cpp b/application/drivermode.cpp index 9efb88ea..d691446d 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -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) { diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index e49493c4..67e808a2 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -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(); @@ -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( 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::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 ); +} diff --git a/application/scenarioloadermode.h b/application/scenarioloadermode.h index db23458d..06c5ceca 100644 --- a/application/scenarioloadermode.h +++ b/application/scenarioloadermode.h @@ -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 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 { ; } diff --git a/application/scenarioloaderuilayer.cpp b/application/scenarioloaderuilayer.cpp index 7b53e543..3670ee4f 100644 --- a/application/scenarioloaderuilayer.cpp +++ b/application/scenarioloaderuilayer.cpp @@ -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(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( -1 ) ) { + draw_list->AddImage( + reinterpret_cast( 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 diff --git a/docs/eu7-format.md b/docs/eu7-format.md new file mode 100644 index 00000000..74496aca --- /dev/null +++ b/docs/eu7-format.md @@ -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 `.scn` istnieje `.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.* diff --git a/eu07-parser/include/eu07/geo/puwg1992.hpp b/eu07-parser/include/eu07/geo/puwg1992.hpp new file mode 100644 index 00000000..565a9def --- /dev/null +++ b/eu07-parser/include/eu07/geo/puwg1992.hpp @@ -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 +#include + +#include +#include +#include +#include +#include +#include +#include + +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 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 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 parsed = parseGeoMapLine(line)) { + PuwgMasterOffset out = *parsed; + out.sourceLine = lineNo + 1; + return out; + } + } + return std::nullopt; +} + +[[nodiscard]] inline std::optional readFirstGeoMap( + const std::filesystem::path& scnPath, + const std::vector& 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 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 diff --git a/eu07-parser/include/eu07/geo/spatial_grid.hpp b/eu07-parser/include/eu07/geo/spatial_grid.hpp new file mode 100644 index 00000000..416cc1af --- /dev/null +++ b/eu07-parser/include/eu07/geo/spatial_grid.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace eu07::geo { + +template +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(key.x * 73856093) ^ + static_cast(key.z * 19349663); + } + }; + + std::unordered_map, CellKeyHash> grid_; + + [[nodiscard]] static int cellIndex(const double value) noexcept { + return static_cast(std::floor(value / static_cast(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& out) const { + out.clear(); + const int cx = cellIndex(point.x); + const int cz = cellIndex(point.z); + int range = static_cast(std::ceil(maxDist / static_cast(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 diff --git a/eu07-parser/include/eu07/nmt/asc_index.hpp b/eu07-parser/include/eu07/nmt/asc_index.hpp new file mode 100644 index 00000000..d223f0a3 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/asc_index.hpp @@ -0,0 +1,323 @@ +#pragma once + +// Jednorazowy indeks nagłówków plików ASC (NMT1) — bbox w PUWG, bez wczytywania siatki. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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( + ftime - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + return std::chrono::duration_cast(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(tmp); + return true; +} + +[[nodiscard]] inline std::optional 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 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 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 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 selectEntriesForCorridor( + const AscIndex& index, + const double northMin, + const double northMax, + const double eastMin, + const double eastMax) { + std::vector 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 diff --git a/eu07-parser/include/eu07/nmt/asc_lookup.hpp b/eu07-parser/include/eu07/nmt/asc_lookup.hpp new file mode 100644 index 00000000..58cbf5da --- /dev/null +++ b/eu07-parser/include/eu07/nmt/asc_lookup.hpp @@ -0,0 +1,152 @@ +#pragma once + +// NMT1 (ASC): wyszukiwanie komorki w promieniu od probki profilu toru. + +#include +#include +#include +#include + +#include +#include +#include +#include + +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(std::floor((east - header.xll) / header.cellsize)); + outRow = static_cast(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(row) + 0.5) * header.cellsize; + const double east = header.xll + (static_cast(col) + 0.5) * header.cellsize; + return { + master.eastM - east, + 0.0, + north - master.northM, + }; +} + +} // namespace detail + +inline void fillProfileNmtHeights( + std::vector& 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(std::ceil(radiusM / cell)) + 1; + + double bestDistSq = radiusSq + 1.0; + std::optional 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 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& 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 diff --git a/eu07-parser/include/eu07/nmt/asc_ram.hpp b/eu07-parser/include/eu07/nmt/asc_ram.hpp new file mode 100644 index 00000000..9b937248 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/asc_ram.hpp @@ -0,0 +1,228 @@ +#pragma once + +// Kafel ASC w RAM + cache między trasami (lazy load per kafel). + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::nmt { + +struct AscTileRam { + AscHeader header{}; + std::shared_ptr> heights; + std::filesystem::path relativePath; + + [[nodiscard]] std::size_t index(const int row, const int col) const { + return static_cast(row) * static_cast(header.ncols) + static_cast(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 heightAt(const int row, const int col) const { + if (!validAt(row, col)) { + return std::nullopt; + } + return static_cast((*heights)[index(row, col)]); + } +}; + +namespace detail { + +inline std::shared_ptr> makeMutableHeightsBuffer(const AscHeader& header) { + const std::size_t cellCount = + static_cast(header.nrows) * static_cast(header.ncols); + return std::make_shared>( + cellCount, std::numeric_limits::quiet_NaN()); +} + +inline std::vector 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 buffer(static_cast(size) + 1); + if (!file.read(buffer.data(), size)) { + return {}; + } + buffer[static_cast(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(*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(*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(fastParseDouble(&ptr, endPtr)); + } else if (h == 1) { + header.nrows = static_cast(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 loadAscTileRam( + const AscIndexEntry& entry, + const std::filesystem::path& ascPath) { + std::vector 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> 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(row) * static_cast(tile.header.ncols) + + static_cast(col); + if (std::abs(value - tile.header.nodata) <= 0.1) { + (*heights)[idx] = std::numeric_limits::quiet_NaN(); + } else { + (*heights)[idx] = static_cast(value); + } + } + } + + return tile; +} + +} // namespace detail + +struct AscTileCache { + std::unordered_map>> heightsByPath; + std::unordered_map 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 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 diff --git a/eu07-parser/include/eu07/nmt/parallel.hpp b/eu07-parser/include/eu07/nmt/parallel.hpp new file mode 100644 index 00000000..2e160623 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/parallel.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +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 diff --git a/eu07-parser/include/eu07/nmt/profile_export.hpp b/eu07-parser/include/eu07/nmt/profile_export.hpp new file mode 100644 index 00000000..2bf2e03e --- /dev/null +++ b/eu07-parser/include/eu07/nmt/profile_export.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace eu07::nmt { + +struct ProfileCsvOptions { + bool includeRouteColumn = false; +}; + +inline void writeProfileCsv( + const std::filesystem::path& outPath, + const std::vector& 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> 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& samples) { + writeProfileCsv(outPath, samples, {.includeRouteColumn = true}); +} + +inline void writeCombinedProfileXyz( + const std::filesystem::path& outPath, + const std::vector& 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& 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 diff --git a/eu07-parser/include/eu07/nmt/profile_height.hpp b/eu07-parser/include/eu07/nmt/profile_height.hpp new file mode 100644 index 00000000..733f71b5 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/profile_height.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace eu07::nmt { + +[[nodiscard]] inline double profileSampleHeightZ(const RouteChainageSample& sample) { + if (sample.nmtHeight) { + return *sample.nmtHeight; + } + return sample.trackHeight; +} + +} // namespace eu07::nmt diff --git a/eu07-parser/include/eu07/nmt/profile_slope.hpp b/eu07-parser/include/eu07/nmt/profile_slope.hpp new file mode 100644 index 00000000..d0c323a6 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/profile_slope.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +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 slopePermilleOverChainageSegment( + const std::vector& samples, + const std::size_t routeStart, + const std::size_t routeEnd, + const double segStartChainage, + const double segEndChainage, + std::optional& outEndIndex) { + std::optional startIndex; + std::optional 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> slopePromilleEvery50m( + const std::vector& samples) { + std::vector> 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 endIndex; + const std::optional slope = slopePermilleOverChainageSegment( + samples, + routeStart, + routeEnd, + segStart, + segEndChainage, + endIndex); + if (slope && endIndex) { + slopes[*endIndex] = *slope; + } + } + + routeStart = routeEnd; + } + + return slopes; +} + +} // namespace eu07::nmt diff --git a/eu07-parser/include/eu07/nmt/profile_types.hpp b/eu07-parser/include/eu07/nmt/profile_types.hpp new file mode 100644 index 00000000..d5015e8c --- /dev/null +++ b/eu07-parser/include/eu07/nmt/profile_types.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include +#include + +namespace eu07::nmt { + +struct RouteChainageSample { + double chainageM = 0.0; + scene::Vec3 sim{}; + geo::PuwgPoint geo{}; + double trackHeight = 0.0; + std::optional nmtHeight; + int routeIndex = 0; +}; + +struct ProfileNmtResult { + std::size_t samplesWithNmt = 0; + std::size_t tilesLoaded = 0; +}; + +} // namespace eu07::nmt diff --git a/eu07-parser/include/eu07/nmt/progress.hpp b/eu07-parser/include/eu07/nmt/progress.hpp new file mode 100644 index 00000000..62fe29d6 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/progress.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +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(now - lastPrint_).count() < 200) { + return; + } + lastPrint_ = now; + + const int pct = static_cast((100.0 * static_cast(current_)) / static_cast(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 diff --git a/eu07-parser/include/eu07/nmt/route_geometry.hpp b/eu07-parser/include/eu07/nmt/route_geometry.hpp new file mode 100644 index 00000000..d51210fa --- /dev/null +++ b/eu07-parser/include/eu07/nmt/route_geometry.hpp @@ -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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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 spans; + std::vector 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& spans, + std::vector& 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(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(i) / static_cast(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* 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* beziers = detail::beziersForSegment(doc, segment); + if (beziers == nullptr || beziers->empty()) { + continue; + } + + const scene::Vec3 origin = detail::headerOffsetForSegment(doc, segment); + const std::vector& 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 sampleRouteCenterlineFromGeometry( + const RouteGeometry& geometry, + const geo::PuwgMasterOffset& master, + const double stepM) { + std::vector 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 sampleRouteCenterlineEvery( + const scene::SceneDocument& doc, + const scene::TrackRouteBuildResult& routes, + const std::size_t routeIndex, + const geo::PuwgMasterOffset& master, + const double stepM) { + std::vector samples; + if (routeIndex >= routes.routes.size() || stepM <= 0.0) { + return samples; + } + + const scene::TrackRoute& route = routes.routes[routeIndex]; + std::vector 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* beziers = detail::beziersForSegment(doc, segment); + if (beziers == nullptr || beziers->empty()) { + continue; + } + + const scene::Vec3 origin = detail::headerOffsetForSegment(doc, segment); + const std::vector& 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& 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 diff --git a/eu07-parser/include/eu07/nmt/terrain.hpp b/eu07-parser/include/eu07/nmt/terrain.hpp new file mode 100644 index 00000000..1929ec2e --- /dev/null +++ b/eu07-parser/include/eu07/nmt/terrain.hpp @@ -0,0 +1,14 @@ +#pragma once + +// Zbiorcze include modulow obrobki terenu (NMT + profil toru). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/eu07-parser/include/eu07/nmt/terrain_cli.hpp b/eu07-parser/include/eu07/nmt/terrain_cli.hpp new file mode 100644 index 00000000..caa33909 --- /dev/null +++ b/eu07-parser/include/eu07/nmt/terrain_cli.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace eu07::nmt { + +struct TerrainProfileOptions { + std::optional 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 diff --git a/eu07-parser/include/eu07/nmt/terrain_pipeline.hpp b/eu07-parser/include/eu07/nmt/terrain_pipeline.hpp new file mode 100644 index 00000000..c5dbd97c --- /dev/null +++ b/eu07-parser/include/eu07/nmt/terrain_pipeline.hpp @@ -0,0 +1,102 @@ +#pragma once + +// Pipeline: geometria toru -> profil co N m -> NMT z ASC -> eksport XYZ. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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 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 samples = + sampleRouteCenterlineFromGeometry(geometry, master, options.profileStepM); + for (RouteChainageSample& sample : samples) { + sample.routeIndex = static_cast(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(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 diff --git a/eu07-parser/include/eu07/parser.hpp b/eu07-parser/include/eu07/parser.hpp new file mode 100644 index 00000000..ae4b38b8 --- /dev/null +++ b/eu07-parser/include/eu07/parser.hpp @@ -0,0 +1,500 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Plik_tekstowy +// + dyrektywy startera (//$…) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(text.front()))) { + text.remove_prefix(1); + } +} + +inline void tokenizeInto( + std::string_view buffer, + std::vector& 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(text.front())) && + text.front() != '"' && + text.front() != '[') { + text.remove_prefix(1); + } + const std::size_t len = static_cast(text.data() - start); + if (len > 0) { + out.push_back(SourceToken{std::string(start, len), lineFor(tokenStart)}); + } + skipFieldSeparators(text); + } +} + +[[nodiscard]] inline std::vector tokenize(std::string_view text) { + std::vector located; + located.reserve(8); + tokenizeInto(text, located); + std::vector 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, 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(line[tag.size()])); +} + +[[nodiscard]] inline std::optional 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 supportedStarterTags() { + std::array 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& 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 parseStarter( + std::string_view line, + const std::size_t lineNo) { + skipFieldSeparators(line); + const std::optional 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 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(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 starters; + std::vector segments; + std::vector spill; +}; + +[[nodiscard]] inline LogicalPass toLogicalLines(const std::vector& 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 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 scanStarters( + const std::vector& 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 starters; + std::vector tokens; +}; + +[[nodiscard]] inline std::vector tokenizeSegments( + const std::vector& segments) { + std::vector 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 diff --git a/eu07-parser/include/eu07/scene/area.hpp b/eu07-parser/include/eu07/scene/area.hpp new file mode 100644 index 00000000..b20b0dd0 --- /dev/null +++ b/eu07-parser/include/eu07/scene/area.hpp @@ -0,0 +1,46 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_area +// area name tracks endarea + +#include +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/atmo.hpp b/eu07-parser/include/eu07/scene/atmo.hpp new file mode 100644 index 00000000..fad213aa --- /dev/null +++ b/eu07-parser/include/eu07/scene/atmo.hpp @@ -0,0 +1,35 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_atmo +// atmo skyColor fogRangeStart fogRangeEnd fogColor overcast endatmo + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/bake/bake_tree.hpp b/eu07-parser/include/eu07/scene/bake/bake_tree.hpp new file mode 100644 index 00000000..701f3c73 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/bake_tree.hpp @@ -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 + +#include +#include + +#include + +#include + +#include + +#include + + + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + + + +namespace eu07::scene::bake { + + + +struct BakeTreeStats { + + std::size_t modulesBaked = 0; + + std::vector 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; + + + +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 includeStack; + +}; + + + +struct BakePoolState { + + std::mutex queueMutex; + + std::condition_variable queueCv; + + std::deque queue; + + + + std::mutex seenMutex; + + std::unordered_set seen; + + + + std::atomic inFlight{0}; + + std::atomic completed{0}; + + std::atomic 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 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 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 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 + + diff --git a/eu07-parser/include/eu07/scene/bake/compose_pack_models.hpp b/eu07-parser/include/eu07/scene/bake/compose_pack_models.hpp new file mode 100644 index 00000000..79f99dc0 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/compose_pack_models.hpp @@ -0,0 +1,1222 @@ +#pragma once + +// Compose MODL z calego drzewa INCL → world-space do chunku PACK (v7). +// Cache parsowania per plik + kolejka zadan z pulą watkow. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::scene::bake { + +using PackComposeProgressCallback = + std::function; + +struct PackComposeProgress { + PackComposeProgressCallback on_progress; + std::size_t files_visited = 0; + std::size_t models_total = 0; + bool reported_once = false; + std::chrono::steady_clock::time_point last_report {}; +}; + +struct PackFileCacheEntry { + std::vector includes; + IncludePlacement placement; + std::vector base_models; +}; + +struct PackComposeSeed { + std::filesystem::path path; + PackFileCacheEntry entry; +}; + +// Liczniki diagnostyczne compose PACK (watki aktualizuja atomowo). +struct PackComposeStats { + std::atomic files_visited { 0 }; + std::atomic models_emitted { 0 }; + std::atomic cache_hits { 0 }; + std::atomic cache_misses { 0 }; + std::atomic cache_seeded { 0 }; + std::atomic cycles_skipped { 0 }; + std::atomic missing_skipped { 0 }; + std::atomic includes_submitted { 0 }; + std::atomic sink_batches { 0 }; + std::atomic instantiate_fast { 0 }; + std::atomic instantiate_full { 0 }; + std::atomic queue_peak { 0 }; + std::atomic unique_files_cached { 0 }; + std::atomic parse_us { 0 }; + std::atomic instantiate_us { 0 }; + std::atomic finalize_us { 0 }; + std::atomic compose_us { 0 }; + std::atomic threads { 0 }; + std::atomic sections { 0 }; + std::atomic chain_fold_steps { 0 }; + std::atomic chain_fold_hops { 0 }; + std::atomic inc_includes_inlined { 0 }; +}; + +struct PackComposeStatsSnapshot { + std::size_t files_visited = 0; + std::size_t models_emitted = 0; + std::size_t cache_hits = 0; + std::size_t cache_misses = 0; + std::size_t cache_seeded = 0; + std::size_t cycles_skipped = 0; + std::size_t missing_skipped = 0; + std::size_t includes_submitted = 0; + std::size_t sink_batches = 0; + std::size_t instantiate_fast = 0; + std::size_t instantiate_full = 0; + std::size_t queue_peak = 0; + std::size_t unique_files_cached = 0; + std::uint64_t parse_us = 0; + std::uint64_t instantiate_us = 0; + std::uint64_t finalize_us = 0; + std::uint64_t compose_us = 0; + unsigned threads = 0; + std::size_t sections = 0; + std::size_t chain_fold_steps = 0; + std::size_t chain_fold_hops = 0; + std::size_t inc_includes_inlined = 0; +}; + +[[nodiscard]] inline PackComposeStatsSnapshot snapshotPackComposeStats( + const PackComposeStats& stats) { + PackComposeStatsSnapshot out; + out.files_visited = stats.files_visited.load(std::memory_order_relaxed); + out.models_emitted = stats.models_emitted.load(std::memory_order_relaxed); + out.cache_hits = stats.cache_hits.load(std::memory_order_relaxed); + out.cache_misses = stats.cache_misses.load(std::memory_order_relaxed); + out.cache_seeded = stats.cache_seeded.load(std::memory_order_relaxed); + out.cycles_skipped = stats.cycles_skipped.load(std::memory_order_relaxed); + out.missing_skipped = stats.missing_skipped.load(std::memory_order_relaxed); + out.includes_submitted = stats.includes_submitted.load(std::memory_order_relaxed); + out.sink_batches = stats.sink_batches.load(std::memory_order_relaxed); + out.instantiate_fast = stats.instantiate_fast.load(std::memory_order_relaxed); + out.instantiate_full = stats.instantiate_full.load(std::memory_order_relaxed); + out.queue_peak = stats.queue_peak.load(std::memory_order_relaxed); + out.unique_files_cached = stats.unique_files_cached.load(std::memory_order_relaxed); + out.parse_us = stats.parse_us.load(std::memory_order_relaxed); + out.instantiate_us = stats.instantiate_us.load(std::memory_order_relaxed); + out.finalize_us = stats.finalize_us.load(std::memory_order_relaxed); + out.compose_us = stats.compose_us.load(std::memory_order_relaxed); + out.threads = stats.threads.load(std::memory_order_relaxed); + out.sections = stats.sections.load(std::memory_order_relaxed); + out.chain_fold_steps = stats.chain_fold_steps.load(std::memory_order_relaxed); + out.chain_fold_hops = stats.chain_fold_hops.load(std::memory_order_relaxed); + out.inc_includes_inlined = stats.inc_includes_inlined.load(std::memory_order_relaxed); + return out; +} + +inline void printPackComposeStats(const PackComposeStatsSnapshot& stats, std::ostream& out) { + const std::size_t files = stats.files_visited; + const std::size_t models = stats.models_emitted; + const std::size_t hits = stats.cache_hits; + const std::size_t misses = stats.cache_misses; + const std::size_t visits = hits + misses; + const double hit_pct = visits == 0 ? 0.0 : (100.0 * static_cast(hits) / static_cast(visits)); + const std::uint64_t compose_ms = stats.compose_us / 1000; + const std::uint64_t parse_ms = stats.parse_us / 1000; + const std::uint64_t inst_ms = stats.instantiate_us / 1000; + const std::uint64_t fin_ms = stats.finalize_us / 1000; + + out << "[EU7] PACK liczniki:\n" + << " pliki=" << files << " modele=" << models << " sekcje=" << stats.sections << '\n' + << " cache: trafienia=" << hits << " miss=" << misses << " (" << hit_pct + << "%) unikalne=" << stats.unique_files_cached << " seed=" << stats.cache_seeded << '\n' + << " includy=" << stats.includes_submitted << " cykle=" << stats.cycles_skipped + << " brak_pliku=" << stats.missing_skipped << '\n' + << " inst: fast=" << stats.instantiate_fast << " full=" << stats.instantiate_full + << " sink_batchy=" << stats.sink_batches << '\n' + << " kolejka_max=" << stats.queue_peak << " watki=" << stats.threads << '\n' + << " lancuch: skladania=" << stats.chain_fold_hops << " kroki=" << stats.chain_fold_steps + << " inc_inline=" << stats.inc_includes_inlined << '\n' + << " czas_ms: compose=" << compose_ms << " parse=" << parse_ms << " inst=" << inst_ms + << " finalize=" << fin_ms << '\n'; + + const unsigned threads = stats.threads; + const std::size_t queue_peak = stats.queue_peak; + if (compose_ms > 0) { + if (parse_ms * 100 / compose_ms > 60) { + out << " (!) parse dominuje - dysk lub processScene na miss\n"; + } + if (inst_ms * 100 / compose_ms > 50) { + out << " (!) instantiate dominuje - transformy/kopie modeli\n"; + } + if (fin_ms * 100 / compose_ms > 20) { + out << " (!) finalize wolny - merge sekcji 1km\n"; + } + } + if (threads > 1 && queue_peak < threads / 2) { + if (stats.chain_fold_steps > 100) { + out << " (!) plytka kolejka - lancuch liniowy (malo rownolegle), skladanie " + "zredukowalo narzut\n"; + } else { + out << " (!) plytka kolejka - drzewo include malo rownolegle\n"; + } + } + if (visits > 1000 && hit_pct < 40.0) { + out << " (!) niski cache hit - duzo unikalnych plikow .scm\n"; + } + if (stats.instantiate_full > stats.instantiate_fast * 4) { + out << " (!) malo fast_inst - wiekszosc includow ma transform/param\n"; + } + out << std::flush; +} + +inline void printPackComposeStats(const PackComposeStats& stats, std::ostream& out) { + printPackComposeStats(snapshotPackComposeStats(stats), out); +} + +struct PackComposeOptions { + PackComposeProgress* progress = nullptr; + PackComposeStats* stats = nullptr; + unsigned max_threads = 0; + // Root juz sparsowany w bake_tree — bez drugiego parseFile. + const PackComposeSeed* root_seed = nullptr; + // Wynik: sekcje 1 km (serializacja PACK w writeRuntimeModule). + std::vector* pack_batches = nullptr; +}; + +namespace detail { + +// Po tylu hopach skladania wysylamy kontynuacje do kolejki (rownolegle segmenty lancucha). +constexpr std::size_t kMaxChainFoldPerTask = 96; + +[[nodiscard]] inline unsigned resolveComposeThreadCount(const unsigned maxThreads) { + const unsigned hw = + std::thread::hardware_concurrency() == 0 ? 4u : std::thread::hardware_concurrency(); + if (maxThreads == 0) { + return std::max(1u, hw); + } + return std::max(1u, maxThreads); +} + +class ComposeConcurrencyGate { +public: + explicit ComposeConcurrencyGate(const unsigned max_slots) + : max_slots_(std::max(1u, max_slots)) {} + + void acquire() { + std::unique_lock lock(mutex_); + cv_.wait(lock, [this]() { return active_slots_ < max_slots_; }); + ++active_slots_; + } + + void release() { + { + std::lock_guard lock(mutex_); + --active_slots_; + } + cv_.notify_one(); + } + + struct Scope { + ComposeConcurrencyGate& gate; + ~Scope() { + gate.release(); + } + }; + + [[nodiscard]] Scope scoped() { + acquire(); + return Scope { *this }; + } + +private: + unsigned max_slots_; + unsigned active_slots_ = 0; + std::mutex mutex_; + std::condition_variable cv_; +}; + +[[nodiscard]] inline bool transformIsEmpty(const runtime::TransformContext& transform) noexcept { + return ( + transform.originStack.empty() && transform.scaleStack.empty() && + transform.rotation.x == 0.0 && transform.rotation.y == 0.0 && + transform.rotation.z == 0.0 && transform.groupStackDepth == 0); +} + +[[nodiscard]] inline runtime::Vec3 transformPoint( + runtime::Vec3 location, + const runtime::TransformContext& transform) { + if (transform.rotation.y != 0.0) { + const double rad = transform.rotation.y * (3.14159265358979323846 / 180.0); + const double c = std::cos(rad); + const double s = std::sin(rad); + const double x = location.x * c + location.z * s; + const double z = -location.x * s + location.z * c; + location.x = x; + location.z = z; + } + + if (!transform.scaleStack.empty()) { + const runtime::Vec3& scale = transform.scaleStack.back(); + if (scale.x != 0.0) { + location.x *= scale.x; + } + if (scale.y != 0.0) { + location.y *= scale.y; + } + if (scale.z != 0.0) { + location.z *= scale.z; + } + } + + if (!transform.originStack.empty()) { + const runtime::Vec3& origin = transform.originStack.back(); + location.x += origin.x; + location.y += origin.y; + location.z += origin.z; + } + + return location; +} + +inline void composeNodeTransform( + runtime::TransformContext& node, + const runtime::TransformContext& prefix) { + if (transformIsEmpty(prefix)) { + return; + } + + const runtime::Vec3 parentOrigin = + prefix.originStack.empty() ? runtime::Vec3{} : prefix.originStack.back(); + for (runtime::Vec3& offset : node.originStack) { + offset.x += parentOrigin.x; + offset.y += parentOrigin.y; + offset.z += parentOrigin.z; + } + + const runtime::Vec3 parentScale = + prefix.scaleStack.empty() ? runtime::Vec3{1.0, 1.0, 1.0} : prefix.scaleStack.back(); + for (runtime::Vec3& scale : node.scaleStack) { + scale.x *= parentScale.x; + scale.y *= parentScale.y; + scale.z *= parentScale.z; + } + + node.rotation.x += prefix.rotation.x; + node.rotation.y += prefix.rotation.y; + node.rotation.z += prefix.rotation.z; + node.groupStackDepth += prefix.groupStackDepth; +} + +inline void composeModelsWithPrefix( + std::vector& models, + const runtime::TransformContext& prefix) { + if (transformIsEmpty(prefix)) { + return; + } + + for (runtime::RuntimeModelInstance& model : models) { + model.location = transformPoint(model.location, prefix); + model.angles.x += prefix.rotation.x; + model.angles.y += prefix.rotation.y; + model.angles.z += prefix.rotation.z; + composeNodeTransform(model.node.transform, prefix); + model.node.area.center = model.location; + } +} + +inline void bindIncludeName(std::string& text, const std::span parameters) { + std::string bound = eu07::scene::detail::applyIncludeParameters(text, parameters); + for (char& ch : bound) { + ch = static_cast(std::tolower(static_cast(ch))); + } + text = std::move(bound); +} + +inline void bindIncludePath(std::string& text, const std::span parameters) { + text = eu07::scene::detail::applyIncludeParameters(text, parameters); +} + +inline void applyIncludeParametersToModels( + std::vector& models, + const std::span parameters) { + if (parameters.empty()) { + return; + } + + for (runtime::RuntimeModelInstance& model : models) { + bindIncludeName(model.node.name, parameters); + bindIncludeName(model.node.nodeType, parameters); + bindIncludePath(model.modelFile, parameters); + bindIncludePath(model.textureFile, parameters); + } +} + +[[nodiscard]] inline double resolvePlacementParam( + const std::uint8_t paramIndex, + const std::span parameters) { + if (paramIndex == 0) { + return 0.0; + } + + const std::size_t index = static_cast(paramIndex); + if (index < 1 || index > parameters.size()) { + throw std::runtime_error("EU7 PACK: brak parametru p" + std::to_string(paramIndex)); + } + + return std::stod(parameters[index - 1]); +} + +[[nodiscard]] inline runtime::TransformContext placementTransformFromParameters( + const IncludePlacement& binding, + const std::span parameters) { + runtime::TransformContext placement; + if (binding.empty()) { + return placement; + } + + try { + const double originX = resolvePlacementParam(binding.origin_x_param, parameters); + const double originY = resolvePlacementParam(binding.origin_y_param, parameters); + const double originZ = resolvePlacementParam(binding.origin_z_param, parameters); + placement.originStack.push_back({originX, originY, originZ}); + placement.rotation.y = resolvePlacementParam(binding.rotation_y_param, parameters); + } catch (const std::exception&) { + placement = {}; + } + + return placement; +} + +inline void applyIncludePlacementToModels( + std::vector& models, + const IncludePlacement& binding, + const std::span parameters) { + const runtime::TransformContext placement = placementTransformFromParameters(binding, parameters); + composeModelsWithPrefix(models, placement); +} + +using PackModelBatchSink = std::function&&)>; + +[[nodiscard]] inline PackFileCacheEntry makePackFileCacheEntry(const SceneDocument& document) { + PackFileCacheEntry entry; + entry.includes = document.include; + if (const std::optional placement = extractIncludePlacement(document)) { + entry.placement = *placement; + } + entry.base_models.reserve(document.nodeModel.size()); + for (const ParsedNodeModel& parsedModel : document.nodeModel) { + entry.base_models.push_back(bakeModel(parsedModel)); + } + return entry; +} + +class PackSectionAccumulator { +public: + static constexpr unsigned kShardCount = 64; + + void add(std::vector&& batch, PackComposeStats* stats = nullptr) { + if (stats != nullptr) { + stats->sink_batches.fetch_add(1, std::memory_order_relaxed); + } + models_total_ += batch.size(); + std::array< + std::unordered_map< + binary::codec::TerrSectionKey, + std::vector, + binary::codec::TerrSectionKeyHash>, + kShardCount> + staged {}; + for (runtime::RuntimeModelInstance& model : batch) { + const binary::codec::TerrSectionKey sectionKey = + binary::codec::clampTerrSectionKey(binary::codec::terrSectionKey(model.location)); + const std::size_t shard = + binary::codec::TerrSectionKeyHash {}(sectionKey) % kShardCount; + staged[shard][sectionKey].push_back(std::move(model)); + } + for (unsigned shard = 0; shard < kShardCount; ++shard) { + if (staged[shard].empty()) { + continue; + } + std::lock_guard lock(shard_mutex_[shard]); + for (auto& [sectionKey, models] : staged[shard]) { + std::vector& target = shards_[shard][sectionKey]; + target.insert( + target.end(), + std::make_move_iterator(models.begin()), + std::make_move_iterator(models.end())); + } + } + } + + void mergeFrom(PackSectionAccumulator&& other) { + models_total_ += other.models_total_; + other.models_total_ = 0; + for (unsigned shard = 0; shard < kShardCount; ++shard) { + if (other.shards_[shard].empty()) { + continue; + } + std::lock_guard lock(shard_mutex_[shard]); + for (auto& [sectionKey, models] : other.shards_[shard]) { + std::vector& target = shards_[shard][sectionKey]; + target.insert( + target.end(), + std::make_move_iterator(models.begin()), + std::make_move_iterator(models.end())); + } + other.shards_[shard].clear(); + } + } + + [[nodiscard]] std::size_t modelsTotal() const { + return models_total_; + } + + [[nodiscard]] std::vector finalize() { + std::unordered_map< + binary::codec::TerrSectionKey, + std::vector, + binary::codec::TerrSectionKeyHash> + merged; + merged.reserve(512); + + for (unsigned shard = 0; shard < kShardCount; ++shard) { + std::lock_guard lock(shard_mutex_[shard]); + for (auto& [key, models] : shards_[shard]) { + std::vector& target = merged[key]; + target.insert( + target.end(), + std::make_move_iterator(models.begin()), + std::make_move_iterator(models.end())); + } + shards_[shard].clear(); + } + + std::vector batches; + batches.reserve(merged.size()); + for (auto& [key, models] : merged) { + binary::codec::ModelSectionBatch batch; + batch.section = key; + batch.models = std::move(models); + batches.push_back(std::move(batch)); + } + + std::sort( + batches.begin(), + batches.end(), + [](const binary::codec::ModelSectionBatch& a, + const binary::codec::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; + } + +private: + std::array< + std::unordered_map< + binary::codec::TerrSectionKey, + std::vector, + binary::codec::TerrSectionKeyHash>, + kShardCount> + shards_ {}; + std::array shard_mutex_ {}; + std::size_t models_total_ = 0; +}; + +struct PackComposeWorkItem { + std::filesystem::path path; + std::string relativeFile; + std::vector includeStack; + runtime::TransformContext includePrefix; + std::vector includeParameters; + IncludePlacement pending_placement; + std::vector pending_placement_parameters; +}; + +struct PackIncludeFoldPlan { + const ParsedInclude* include = nullptr; + std::string relative_file; + std::vector stack_suffix; +}; + +[[nodiscard]] inline std::vector instantiateModels( + const PackFileCacheEntry& entry, + const runtime::TransformContext& includePrefix, + const std::span includeParameters, + PackComposeStats* stats = nullptr) { + const bool needs_parameters = !includeParameters.empty(); + const bool needs_prefix = !transformIsEmpty(includePrefix); + const bool needs_placement = !entry.placement.empty(); + if (!needs_parameters && !needs_prefix && !needs_placement) { + if (stats != nullptr) { + stats->instantiate_fast.fetch_add(1, std::memory_order_relaxed); + } + return entry.base_models; + } + if (stats != nullptr) { + stats->instantiate_full.fetch_add(1, std::memory_order_relaxed); + } + + std::vector models = entry.base_models; + if (needs_parameters) { + applyIncludeParametersToModels(models, includeParameters); + } + if (needs_prefix) { + composeModelsWithPrefix(models, includePrefix); + } + if (needs_placement) { + applyIncludePlacementToModels(models, entry.placement, includeParameters); + } + return models; +} + +class PackComposeSession { +public: + PackComposeSession( + const unsigned max_threads, + PackComposeProgress* progress, + const std::filesystem::path& scenery_root, + PackComposeStats* stats = nullptr) + : max_threads_(resolveComposeThreadCount(max_threads)) + , gate_(max_threads_) + , progress_(progress) + , scenery_root_(scenery_root) + , stats_(stats) {} + + void seedCache(const std::filesystem::path& resolved, PackFileCacheEntry entry) { + const std::string key = resolved.lexically_normal().generic_string(); + std::unique_lock write_lock(cache_mutex_); + cache_.emplace(key, std::move(entry)); + if (stats_ != nullptr) { + stats_->cache_seeded.fetch_add(1, std::memory_order_relaxed); + stats_->unique_files_cached.store(cache_.size(), std::memory_order_relaxed); + } + } + + void runCompose( + const std::filesystem::path& path, + const std::string& relativeFile, + const std::vector& includeStack, + const runtime::TransformContext& includePrefix, + const std::span includeParameters, + PackModelBatchSink& sink) { + std::mutex queue_mutex; + std::deque queue; + std::condition_variable queue_cv; + std::condition_variable done_cv; + std::atomic pending_tasks { 0 }; + std::atomic stop_workers { false }; + std::exception_ptr first_error; + std::mutex error_mutex; + + auto submit = [&](PackComposeWorkItem item) { + pending_tasks.fetch_add(1, std::memory_order_acq_rel); + std::size_t queue_size = 0; + { + std::lock_guard lock(queue_mutex); + queue.push_back(std::move(item)); + queue_size = queue.size(); + } + if (stats_ != nullptr) { + std::size_t peak = stats_->queue_peak.load(std::memory_order_relaxed); + while (queue_size > peak && + !stats_->queue_peak.compare_exchange_weak( + peak, queue_size, std::memory_order_relaxed, std::memory_order_relaxed)) { + } + } + queue_cv.notify_one(); + }; + + auto finish_task = [&]() { + if (pending_tasks.fetch_sub(1, std::memory_order_acq_rel) == 1) { + done_cv.notify_all(); + } + }; + + auto process_one = [&](PackComposeWorkItem work) { + bool folded_hop = false; + std::size_t task_fold_steps = 0; + while (true) { + const std::filesystem::path resolved = work.path.lexically_normal(); + if (eu07::scene::detail::isIncludeCycle(resolved, work.includeStack)) { + if (stats_ != nullptr) { + stats_->cycles_skipped.fetch_add(1, std::memory_order_relaxed); + } + return; + } + + if (!std::filesystem::exists(resolved)) { + if (work.includeStack.empty()) { + throw std::runtime_error("EU7 PACK: brak pliku: " + resolved.string()); + } + if (stats_ != nullptr) { + stats_->missing_skipped.fetch_add(1, std::memory_order_relaxed); + } + return; + } + + const PackFileCacheEntry& cached = loadCached(resolved); + const auto inst_begin = std::chrono::steady_clock::now(); + std::vector models = instantiateModels( + cached, work.includePrefix, work.includeParameters, stats_); + if (!work.pending_placement.empty()) { + applyIncludePlacementToModels( + models, work.pending_placement, work.pending_placement_parameters); + work.pending_placement = {}; + work.pending_placement_parameters.clear(); + } + if (stats_ != nullptr) { + const auto inst_end = std::chrono::steady_clock::now(); + stats_->instantiate_us.fetch_add( + static_cast( + std::chrono::duration_cast( + inst_end - inst_begin) + .count()), + std::memory_order_relaxed); + } + reportProgress(resolved, models.size()); + + std::optional fold_plan; + std::vector fanout_plans; + + // .inc to szablon placementu — ten sam plik na kazdym segmencie lancucha jest OK. + // Cykl sprawdzamy tylko w obecnym przejsciu inc->inc, nie w historii .scm. + auto is_inc_walk_cycle = [&](const std::filesystem::path& target, + const std::vector& suffix) { + const std::filesystem::path normalized = target.lexically_normal(); + for (const std::filesystem::path& entry : suffix) { + if (entry.lexically_normal() == normalized) { + return true; + } + } + return false; + }; + + auto report_compose_throttle = [&](const std::filesystem::path& hint) { + if (progress_ == nullptr || progress_->on_progress == nullptr) { + return; + } + const auto now = std::chrono::steady_clock::now(); + const bool timed = + !progress_->reported_once || + std::chrono::duration_cast( + now - progress_->last_report) + .count() >= 100; + if (!timed) { + return; + } + std::lock_guard lock(progress_mutex_); + progress_->reported_once = true; + progress_->last_report = now; + progress_->on_progress( + hint, + stats_ != nullptr ? stats_->files_visited.load(std::memory_order_relaxed) + : files_visited_.load(std::memory_order_relaxed), + stats_ != nullptr ? stats_->models_emitted.load(std::memory_order_relaxed) + : models_total_.load(std::memory_order_relaxed)); + }; + + auto note_inc_inline = [&](const std::filesystem::path& inc_path) { + if (stats_ != nullptr) { + const std::size_t steps = + stats_->inc_includes_inlined.fetch_add(1, std::memory_order_relaxed) + + 1; + if ((steps % 50) == 0) { + report_compose_throttle(inc_path); + } + } + }; + + auto absorb_continuations = [&](const std::vector& locals, + const std::string& relative_file, + const std::vector& suffix) { + if (locals.empty()) { + return; + } + if (locals.size() == 1) { + if (!fold_plan.has_value()) { + fold_plan = PackIncludeFoldPlan{locals.front(), relative_file, suffix}; + } + return; + } + for (const ParsedInclude* include : locals) { + fanout_plans.push_back(PackIncludeFoldPlan{include, relative_file, suffix}); + } + }; + + auto walk_inc_chain = [&](const ParsedInclude& start_inc, + std::string relative_file, + std::vector suffix) { + const ParsedInclude* step = &start_inc; + std::vector placement_params = start_inc.parameters; + + while (true) { + const std::filesystem::path inc_path = + eu07::scene::detail::resolveIncludeSourcePath( + scenery_root_, relative_file, step->file) + .lexically_normal(); + if (is_inc_walk_cycle(inc_path, suffix)) { + if (stats_ != nullptr) { + stats_->cycles_skipped.fetch_add(1, std::memory_order_relaxed); + } + return; + } + if (!std::filesystem::exists(inc_path)) { + if (stats_ != nullptr) { + stats_->missing_skipped.fetch_add(1, std::memory_order_relaxed); + } + return; + } + + const PackFileCacheEntry& inc_entry = loadCached(inc_path); + if (!inc_entry.placement.empty()) { + work.pending_placement = inc_entry.placement; + work.pending_placement_parameters = placement_params; + } + if (!inc_entry.base_models.empty()) { + std::vector inc_models = + instantiateModels( + inc_entry, step->siteTransform, placement_params, stats_); + if (!inc_models.empty()) { + sink(std::move(inc_models)); + } + } + note_inc_inline(inc_path); + + const std::string inc_relative = + eu07::scene::detail::relativeSceneryFile(scenery_root_, inc_path); + std::vector non_inc; + non_inc.reserve(inc_entry.includes.size()); + const ParsedInclude* next_inc = nullptr; + for (const ParsedInclude& inner : inc_entry.includes) { + if (eu07::scene::detail::isIncFile(inner.file)) { + next_inc = &inner; + continue; + } + non_inc.push_back(&inner); + } + + if (!non_inc.empty()) { + absorb_continuations(non_inc, inc_relative, suffix); + return; + } + + if (next_inc != nullptr) { + suffix.push_back(inc_path); + relative_file = inc_relative; + placement_params = next_inc->parameters; + step = next_inc; + continue; + } + + return; + } + }; + + auto submit_fold_plan = [&](const PackIncludeFoldPlan& plan) { + PackComposeWorkItem child; + child.path = eu07::scene::detail::resolveIncludeSourcePath( + scenery_root_, plan.relative_file, plan.include->file); + child.relativeFile = + eu07::scene::detail::relativeSceneryFile(scenery_root_, child.path); + child.includeStack = work.includeStack; + child.includeStack.push_back(resolved); + child.includePrefix = plan.include->siteTransform; + child.includeParameters = plan.include->parameters; + child.pending_placement = work.pending_placement; + child.pending_placement_parameters = work.pending_placement_parameters; + submit(std::move(child)); + if (stats_ != nullptr) { + stats_->includes_submitted.fetch_add(1, std::memory_order_relaxed); + } + }; + + const bool parent_has_branching = cached.includes.size() > 1; + std::vector local_continuations; + local_continuations.reserve(cached.includes.size()); + for (const ParsedInclude& include : cached.includes) { + if (eu07::scene::detail::isIncFile(include.file)) { + walk_inc_chain(include, work.relativeFile, {}); + if (fold_plan.has_value() && parent_has_branching) { + submit_fold_plan(*fold_plan); + fold_plan.reset(); + } + continue; + } + local_continuations.push_back(&include); + } + absorb_continuations(local_continuations, work.relativeFile, {}); + + auto resolve_plan_path = [&](const PackIncludeFoldPlan& plan) { + return eu07::scene::detail::resolveIncludeSourcePath( + scenery_root_, plan.relative_file, plan.include->file) + .lexically_normal(); + }; + + if (fold_plan.has_value() && !fanout_plans.empty()) { + const std::filesystem::path fold_path = resolve_plan_path(*fold_plan); + fanout_plans.erase( + std::remove_if( + fanout_plans.begin(), + fanout_plans.end(), + [&](const PackIncludeFoldPlan& plan) { + return resolve_plan_path(plan) == fold_path; + }), + fanout_plans.end()); + } + + if (fold_plan.has_value() && fanout_plans.empty()) { + if (!models.empty()) { + sink(std::move(models)); + } + const PackIncludeFoldPlan& plan = *fold_plan; + const std::filesystem::path next_path = + eu07::scene::detail::resolveIncludeSourcePath( + scenery_root_, plan.relative_file, plan.include->file) + .lexically_normal(); + if (next_path == resolved) { + if (stats_ != nullptr) { + stats_->cycles_skipped.fetch_add(1, std::memory_order_relaxed); + } + if (!models.empty()) { + sink(std::move(models)); + } + return; + } + work.includeStack.push_back(resolved); + work.path = next_path; + work.relativeFile = + eu07::scene::detail::relativeSceneryFile(scenery_root_, work.path); + work.includePrefix = plan.include->siteTransform; + work.includeParameters = plan.include->parameters; + ++task_fold_steps; + if (stats_ != nullptr) { + stats_->chain_fold_steps.fetch_add(1, std::memory_order_relaxed); + if (!folded_hop) { + folded_hop = true; + stats_->chain_fold_hops.fetch_add(1, std::memory_order_relaxed); + } + } + if (task_fold_steps >= kMaxChainFoldPerTask) { + submit(std::move(work)); + if (stats_ != nullptr) { + stats_->includes_submitted.fetch_add(1, std::memory_order_relaxed); + } + if (!models.empty()) { + sink(std::move(models)); + } + return; + } + continue; + } + + if (!fanout_plans.empty()) { + for (const PackIncludeFoldPlan& plan : fanout_plans) { + submit_fold_plan(plan); + } + } + + if (!models.empty()) { + sink(std::move(models)); + } + return; + } + }; + + auto worker_loop = [&]() { + while (true) { + PackComposeWorkItem work; + { + std::unique_lock lock(queue_mutex); + queue_cv.wait(lock, [&]() { + return stop_workers.load(std::memory_order_acquire) || !queue.empty(); + }); + if (stop_workers.load(std::memory_order_acquire) && queue.empty()) { + return; + } + if (queue.empty()) { + continue; + } + work = std::move(queue.front()); + queue.pop_front(); + } + + try { + process_one(std::move(work)); + } catch (...) { + { + std::lock_guard lock(error_mutex); + if (!first_error) { + first_error = std::current_exception(); + } + } + stop_workers.store(true, std::memory_order_release); + queue_cv.notify_all(); + } + finish_task(); + } + }; + + std::vector workers; + workers.reserve(max_threads_); + for (unsigned worker = 0; worker < max_threads_; ++worker) { + workers.emplace_back(worker_loop); + } + + PackComposeWorkItem root; + root.path = path; + root.relativeFile = relativeFile; + root.includeStack = includeStack; + root.includePrefix = includePrefix; + root.includeParameters.assign(includeParameters.begin(), includeParameters.end()); + submit(std::move(root)); + + { + std::unique_lock lock(queue_mutex); + done_cv.wait(lock, [&]() { + return pending_tasks.load(std::memory_order_acquire) == 0 || first_error != nullptr; + }); + } + + stop_workers.store(true, std::memory_order_release); + { + std::lock_guard lock(queue_mutex); + while (!queue.empty()) { + queue.pop_front(); + finish_task(); + } + } + queue_cv.notify_all(); + for (std::thread& worker : workers) { + if (worker.joinable()) { + worker.join(); + } + } + + if (first_error) { + std::rethrow_exception(first_error); + } + } + + [[nodiscard]] std::size_t cacheHits() const { + return cache_hits_.load(std::memory_order_relaxed); + } + + [[nodiscard]] std::size_t cacheMisses() const { + return cache_misses_.load(std::memory_order_relaxed); + } + +private: + [[nodiscard]] PackFileCacheEntry buildCacheEntry(const std::filesystem::path& resolved) { + const auto parse_begin = std::chrono::steady_clock::now(); + const ParseResult parsed = parseFile(resolved); + SceneProcessOptions options; + options.expandIncludes = false; + const SceneDocument document = + processScene(parsed, scenery_root_, options).document; + PackFileCacheEntry entry = makePackFileCacheEntry(document); + if (stats_ != nullptr) { + const auto parse_end = std::chrono::steady_clock::now(); + stats_->parse_us.fetch_add( + static_cast( + std::chrono::duration_cast(parse_end - parse_begin) + .count()), + std::memory_order_relaxed); + } + return entry; + } + + [[nodiscard]] const PackFileCacheEntry& loadCached(const std::filesystem::path& resolved) { + const std::string key = resolved.generic_string(); + { + std::shared_lock read_lock(cache_mutex_); + if (const auto found = cache_.find(key); found != cache_.end()) { + noteCacheHit(); + return found->second; + } + } + + const ComposeConcurrencyGate::Scope gate_scope = gate_.scoped(); + { + std::shared_lock read_lock(cache_mutex_); + if (const auto found = cache_.find(key); found != cache_.end()) { + noteCacheHit(); + return found->second; + } + } + + PackFileCacheEntry entry = buildCacheEntry(resolved); + + std::unique_lock write_lock(cache_mutex_); + const auto [it, inserted] = cache_.emplace(key, std::move(entry)); + if (!inserted) { + noteCacheHit(); + } else { + noteCacheMiss(); + } + if (stats_ != nullptr) { + stats_->unique_files_cached.store(cache_.size(), std::memory_order_relaxed); + } + return it->second; + } + + void noteCacheHit() { + cache_hits_.fetch_add(1, std::memory_order_relaxed); + if (stats_ != nullptr) { + stats_->cache_hits.fetch_add(1, std::memory_order_relaxed); + } + } + + void noteCacheMiss() { + cache_misses_.fetch_add(1, std::memory_order_relaxed); + if (stats_ != nullptr) { + stats_->cache_misses.fetch_add(1, std::memory_order_relaxed); + } + } + + void reportProgress(const std::filesystem::path& file, const std::size_t local_models) { + const std::size_t files_visited = files_visited_.fetch_add(1, std::memory_order_relaxed) + 1; + const std::size_t models_total = + models_total_.fetch_add(local_models, std::memory_order_relaxed) + local_models; + + if (stats_ != nullptr) { + stats_->files_visited.store(files_visited, std::memory_order_relaxed); + stats_->models_emitted.store(models_total, std::memory_order_relaxed); + } + + if (progress_ == nullptr || progress_->on_progress == nullptr) { + return; + } + + const auto now = std::chrono::steady_clock::now(); + const bool periodic = (files_visited % 10) == 0; + const bool timed = + !progress_->reported_once || + std::chrono::duration_cast(now - progress_->last_report) + .count() >= 100; + if (!periodic && !timed) { + return; + } + + std::lock_guard lock(progress_mutex_); + progress_->files_visited = files_visited; + progress_->models_total = models_total; + progress_->reported_once = true; + progress_->last_report = now; + progress_->on_progress(file, files_visited, models_total); + } + + unsigned max_threads_; + ComposeConcurrencyGate gate_; + PackComposeProgress* progress_; + std::filesystem::path scenery_root_; + PackComposeStats* stats_ = nullptr; + std::shared_mutex cache_mutex_; + std::unordered_map cache_; + std::mutex progress_mutex_; + std::atomic files_visited_ { 0 }; + std::atomic models_total_ { 0 }; + std::atomic cache_hits_ { 0 }; + std::atomic cache_misses_ { 0 }; +}; + +} // namespace detail + +using detail::makePackFileCacheEntry; + +[[nodiscard]] inline std::size_t composePackModels( + const std::filesystem::path& path, + const std::filesystem::path& sceneryRoot, + const std::string& relativeFile, + const PackComposeOptions& options, + const std::vector& includeStack = {}, + const runtime::TransformContext& includePrefix = {}, + const std::span includeParameters = {}) { + const auto compose_begin = std::chrono::steady_clock::now(); + detail::PackComposeSession session( + options.max_threads, options.progress, sceneryRoot, options.stats); + if (options.root_seed != nullptr) { + session.seedCache(options.root_seed->path, options.root_seed->entry); + } + + if (options.pack_batches == nullptr) { + throw std::invalid_argument("EU7 PACK: pack_batches jest wymagane"); + } + + const unsigned thread_count = detail::resolveComposeThreadCount(options.max_threads); + if (options.stats != nullptr) { + options.stats->threads.store(thread_count, std::memory_order_relaxed); + } + std::vector thread_accumulators(thread_count); + std::atomic next_worker_id { 0 }; + + detail::PackModelBatchSink sink { + [&thread_accumulators, &next_worker_id, stats = options.stats]( + std::vector&& batch) { + thread_local unsigned worker_index = static_cast(-1); + if (worker_index == static_cast(-1)) { + worker_index = next_worker_id.fetch_add(1, std::memory_order_relaxed); + } + if (worker_index < thread_accumulators.size()) { + thread_accumulators[worker_index].add(std::move(batch), stats); + } else { + thread_accumulators[worker_index % thread_accumulators.size()].add( + std::move(batch), stats); + } + } }; + session.runCompose(path, relativeFile, includeStack, includePrefix, includeParameters, sink); + + const auto finalize_begin = std::chrono::steady_clock::now(); + detail::PackSectionAccumulator merged; + for (detail::PackSectionAccumulator& accumulator : thread_accumulators) { + merged.mergeFrom(std::move(accumulator)); + } + *options.pack_batches = merged.finalize(); + const auto compose_end = std::chrono::steady_clock::now(); + + if (options.stats != nullptr) { + options.stats->sections.store(options.pack_batches->size(), std::memory_order_relaxed); + options.stats->finalize_us.fetch_add( + static_cast(std::chrono::duration_cast( + compose_end - finalize_begin) + .count()), + std::memory_order_relaxed); + options.stats->compose_us.store( + static_cast( + std::chrono::duration_cast(compose_end - compose_begin) + .count()), + std::memory_order_relaxed); + if (options.stats->models_emitted.load(std::memory_order_relaxed) == 0) { + options.stats->models_emitted.store(merged.modelsTotal(), std::memory_order_relaxed); + } + } + return merged.modelsTotal(); +} + +} // namespace eu07::scene::bake diff --git a/eu07-parser/include/eu07/scene/bake/dynamic.hpp b/eu07-parser/include/eu07/scene/bake/dynamic.hpp new file mode 100644 index 00000000..629f1198 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/dynamic.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +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(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 diff --git a/eu07-parser/include/eu07/scene/bake/event.hpp b/eu07-parser/include/eu07/scene/bake/event.hpp new file mode 100644 index 00000000..6ec6e8ac --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/event.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +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& 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(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 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 bakeEvents(const SceneDocument& document) { + std::vector events; + events.reserve(document.event.size()); + for (const DirectiveBlock& block : document.event) { + events.push_back(bakeEvent(block)); + } + return events; +} + +} // namespace eu07::scene::bake diff --git a/eu07-parser/include/eu07/scene/bake/eventlauncher.hpp b/eu07-parser/include/eu07/scene/bake/eventlauncher.hpp new file mode 100644 index 00000000..6940f371 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/eventlauncher.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +#include + +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(delta) % 100; + launcher.launchHour = static_cast(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 diff --git a/eu07-parser/include/eu07/scene/bake/geometry.hpp b/eu07-parser/include/eu07/scene/bake/geometry.hpp new file mode 100644 index 00000000..7813bf30 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/geometry.hpp @@ -0,0 +1,188 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +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& 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 triangulateTriangles( + const std::vector& source) { + std::vector 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 triangulateTriangleFan( + const std::vector& source) { + std::vector 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 triangulateTriangleStrip( + const std::vector& source) { + std::vector 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(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(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(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(std::sqrt(radiusSq)); +} + +[[nodiscard]] inline runtime::LightingData bakeLighting(const NodeMaterial& material) { + runtime::LightingData lit; + lit.ambient = { + static_cast(material.ambient.r / 255.0), + static_cast(material.ambient.g / 255.0), + static_cast(material.ambient.b / 255.0), + 1.f}; + lit.diffuse = { + static_cast(material.diffuse.r / 255.0), + static_cast(material.diffuse.g / 255.0), + static_cast(material.diffuse.b / 255.0), + 1.f}; + lit.specular = { + static_cast(material.specular.r / 255.0), + static_cast(material.specular.g / 255.0), + static_cast(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 diff --git a/eu07-parser/include/eu07/scene/bake/lines.hpp b/eu07-parser/include/eu07/scene/bake/lines.hpp new file mode 100644 index 00000000..0e003681 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/lines.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace eu07::scene::bake { + +[[nodiscard]] inline runtime::RuntimeLinesNode bakeLinesNode( + const NodeHeader& header, + const std::string& nodeType, + const MaterialRgb& color, + const double thickness, + std::vector vertices) { + runtime::RuntimeLinesNode lines; + lines.node = bakeBasicNode(header, nodeType); + lines.lineWidth = static_cast(std::min(30.0, thickness)); + lines.lighting.diffuse = { + static_cast(color.r / 255.0), + static_cast(color.g / 255.0), + static_cast(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 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 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 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 diff --git a/eu07-parser/include/eu07/scene/bake/memcell.hpp b/eu07-parser/include/eu07/scene/bake/memcell.hpp new file mode 100644 index 00000000..1b5c8e59 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/memcell.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/bake/mesh.hpp b/eu07-parser/include/eu07/scene/bake/mesh.hpp new file mode 100644 index 00000000..fe1ebd53 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/mesh.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace eu07::scene::bake { + +[[nodiscard]] inline runtime::RuntimeShapeNode bakeShape( + const NodeHeader& header, + const std::string& nodeType, + const std::string& texturePath, + const std::optional& material, + std::vector 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{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 diff --git a/eu07-parser/include/eu07/scene/bake/model.hpp b/eu07-parser/include/eu07/scene/bake/model.hpp new file mode 100644 index 00000000..733e112c --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/model.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +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(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::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 diff --git a/eu07-parser/include/eu07/scene/bake/module.hpp b/eu07-parser/include/eu07/scene/bake/module.hpp new file mode 100644 index 00000000..670606aa --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/module.hpp @@ -0,0 +1,164 @@ +#pragma once + +// Jeden plik tekstowy (.scn/.inc) → jeden moduł runtime (RuntimeModule). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace eu07::scene::bake { + +struct ModuleInclude { + std::uint32_t sourceLine = 0; + std::string sourcePath; + std::string binaryPath; + std::vector parameters; + runtime::TransformContext siteTransform; +}; + +struct RuntimeModule { + std::vector 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(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(document.firstInit.size()); + + if (const std::optional placement = extractIncludePlacement(document)) { + module.includePlacement = *placement; + } + + return module; +} + +} // namespace eu07::scene::bake diff --git a/eu07-parser/include/eu07/scene/bake/sound.hpp b/eu07-parser/include/eu07/scene/bake/sound.hpp new file mode 100644 index 00000000..5cf4b2b1 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/sound.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/bake/track.hpp b/eu07-parser/include/eu07/scene/bake/track.hpp new file mode 100644 index 00000000..e56d58ac --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/track.hpp @@ -0,0 +1,330 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace eu07::scene::bake { + +namespace detail { + +inline void appendTrackEventKeyword( + std::vector>& tail, + const std::string_view key, + const std::optional& value) { + if (value && !value->empty()) { + tail.emplace_back(std::string(key), *value); + } +} + +inline void appendTrackOptionals( + std::vector>& 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>& 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::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& 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(parsed.length); + track.trackWidth = static_cast(parsed.gauge); + track.friction = static_cast(parsed.friction); + track.soundDistance = static_cast(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(parsed.visibility->railTexLength); + vis.material2 = parsed.visibility->ballastTexture; + vis.texHeight1 = static_cast(parsed.visibility->ballastHeight); + vis.texWidth = static_cast(parsed.visibility->ballastWidth); + vis.texSlope = static_cast(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(parsed.length); + track.trackWidth = static_cast(parsed.gauge); + track.friction = static_cast(parsed.friction); + track.soundDistance = static_cast(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(parsed.visibility->mainRailTexLength); + vis.material2 = parsed.visibility->switchRailTexture; + vis.texHeight1 = static_cast(parsed.visibility->unusedHeight); + vis.texWidth = static_cast(parsed.visibility->unusedWidth); + vis.texSlope = static_cast(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(parsed.length); + track.trackWidth = static_cast(parsed.roadWidth); + track.friction = static_cast(parsed.friction); + track.soundDistance = static_cast(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(parsed.visibility->surfaceTexLength); + vis.material2 = parsed.visibility->shoulderTexture; + vis.texHeight1 = static_cast(parsed.visibility->shoulderHeight); + vis.texWidth = static_cast(parsed.visibility->shoulderWidth); + vis.texSlope = static_cast(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(parsed.length); + track.trackWidth = static_cast(parsed.roadWidth); + track.friction = static_cast(parsed.friction); + track.soundDistance = static_cast(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(parsed.visibility->surfaceTexLength); + vis.material2 = parsed.visibility->shoulderTexture; + vis.texHeight1 = static_cast(parsed.visibility->shoulderHeight); + vis.texWidth = static_cast(parsed.visibility->shoulderWidth); + vis.texSlope = static_cast(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(parsed.length); + track.trackWidth = static_cast(parsed.width); + track.friction = static_cast(parsed.friction); + track.soundDistance = static_cast(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(parsed.riverVisibility->waterTexLength); + vis.material2 = parsed.riverVisibility->bankTexture; + vis.texHeight1 = static_cast(parsed.riverVisibility->bankHeight); + vis.texWidth = static_cast(parsed.riverVisibility->leftBankWidth); + vis.texSlope = static_cast(parsed.riverVisibility->rightBankWidth); + track.visibility = vis; + } else if (parsed.railVisibility) { + runtime::TrackVisibility vis; + vis.material1 = parsed.railVisibility->railTexture; + vis.texLength = static_cast(parsed.railVisibility->railTexLength); + vis.material2 = parsed.railVisibility->ballastTexture; + vis.texHeight1 = static_cast(parsed.railVisibility->ballastHeight); + vis.texWidth = static_cast(parsed.railVisibility->ballastWidth); + vis.texSlope = static_cast(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 diff --git a/eu07-parser/include/eu07/scene/bake/traction.hpp b/eu07-parser/include/eu07/scene/bake/traction.hpp new file mode 100644 index 00000000..31f7d333 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/traction.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + +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(parsed.nominalVoltage); + traction.maxCurrent = static_cast(parsed.maxCurrent); + + traction.resistivityLegacy = parsed.resistivity; + double resistivity = parsed.resistivity; + if (resistivity == 0.01) { + resistivity = 0.075; + } + traction.resistivityOhmPerM = static_cast(resistivity * 0.001); + + traction.materialRaw = parsed.materialRaw; + traction.material = mapWireMaterial(parsed.materialKind); + traction.wireThickness = static_cast(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(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 diff --git a/eu07-parser/include/eu07/scene/bake/traction_power.hpp b/eu07-parser/include/eu07/scene/bake/traction_power.hpp new file mode 100644 index 00000000..8a120d2f --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/traction_power.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +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(parsed.nominalVoltage); + source.voltageFrequency = static_cast(parsed.voltageFrequency); + source.maxOutputCurrent = static_cast(parsed.maxOutputCurrent); + source.fastFuseTimeout = static_cast(parsed.fastFuseTimeout); + source.fastFuseRepetition = static_cast(parsed.fastFuseRepetition); + source.slowFuseTimeout = static_cast(parsed.slowFuseTimeout); + source.modifier = mapPowerModifier(parsed); + + source.internalResistanceLegacy = parsed.internalResistance; + float internalResistance = static_cast(parsed.internalResistance); + if (internalResistance < 0.1f) { + internalResistance = 0.2f; + } + source.internalResistance = internalResistance; + + return source; +} + +} // namespace eu07::scene::bake diff --git a/eu07-parser/include/eu07/scene/bake/trainset.hpp b/eu07-parser/include/eu07/scene/bake/trainset.hpp new file mode 100644 index 00000000..3e229273 --- /dev/null +++ b/eu07-parser/include/eu07/scene/bake/trainset.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +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 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(offset); + trainset.velocity = static_cast(velocity); + return true; +} + +} // namespace detail + +[[nodiscard]] inline std::vector bakeTrainsets( + const SceneDocument& document, + const std::vector& dynamics) { + std::vector 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(-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 diff --git a/eu07-parser/include/eu07/scene/binary/codec.hpp b/eu07-parser/include/eu07/scene/binary/codec.hpp new file mode 100644 index 00000000..311cfca3 --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/codec.hpp @@ -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 +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::scene::binary { + +inline constexpr std::uint32_t kVersion = kVersionLegacy; + +inline constexpr std::array kChunkIncl{'I', 'N', 'C', 'L'}; +inline constexpr std::array 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 id{}; + std::uint32_t size = 0; + bool recognized = false; + }; + std::vector 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(inc.line)); + io::writeU32(out, static_cast(inc.file.size())); + out.write(inc.file.data(), static_cast(inc.file.size())); + io::writeU32(out, static_cast(inc.parameters.size())); + for (const std::string& param : inc.parameters) { + io::writeU32(out, static_cast(param.size())); + out.write(param.data(), static_cast(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 +inline void writeTrackRecord(std::ostream& out, const TrackKind kind, const TrackNode& node) { + io::writeU8(out, static_cast(kind)); + io::writeU32(out, static_cast(node.header.name.size())); + out.write(node.header.name.data(), static_cast(node.header.name.size())); + io::writeVec3(out, node.header.originOffset); + io::writeF64(out, node.length); + io::writeU32(out, static_cast(node.beziers.size())); + for (const TrackBezier& bez : node.beziers) { + writeBezier(out, bez); + } +} + +template +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 buildInclPayload(const SceneDocument& doc) { + std::ostringstream inclOut(std::ios::binary); + io::writeU32(inclOut, static_cast(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 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 inclPayload = detail::buildInclPayload(doc); + const std::vector 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(inclPayload.size())); + out.write(inclPayload.data(), static_cast(inclPayload.size())); + + io::writeChunkHeader(out, kChunkTrak, 8 + static_cast(trakPayload.size())); + out.write(trakPayload.data(), static_cast(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 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 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(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(payloadSize)) { + switch (static_cast(io::readU8(in))) { + case TrackKind::Normal: + detail::readTrackBody(in, [&](ParsedTrackNormal node) { + loaded.document.nodeTrackNormal.push_back(std::move(node)); + }); + break; + case TrackKind::Switch: + detail::readTrackBody(in, [&](ParsedTrackSwitch node) { + loaded.document.nodeTrackSwitch.push_back(std::move(node)); + }); + break; + case TrackKind::Road: + detail::readTrackBody(in, [&](ParsedTrackRoad node) { + loaded.document.nodeTrackRoad.push_back(std::move(node)); + }); + break; + case TrackKind::Cross: + detail::readTrackBody(in, [&](ParsedTrackCross node) { + loaded.document.nodeTrackCross.push_back(std::move(node)); + }); + break; + case TrackKind::Other: + detail::readTrackBody(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(payloadSize), std::ios::cur); + loaded.chunks.push_back(chunkSummary); + } + + return loaded; +} + +} // namespace eu07::scene::binary diff --git a/eu07-parser/include/eu07/scene/binary/common.hpp b/eu07-parser/include/eu07/scene/binary/common.hpp new file mode 100644 index 00000000..23783a05 --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/common.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::scene::binary { + +inline constexpr std::array 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(std::tolower(static_cast(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 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 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 diff --git a/eu07-parser/include/eu07/scene/binary/detokenize.hpp b/eu07-parser/include/eu07/scene/binary/detokenize.hpp new file mode 100644 index 00000000..fb3af6dc --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/detokenize.hpp @@ -0,0 +1,717 @@ +#pragma once + +// Runtime → strumień SourceToken (jak po eu07::parseFile) z odtworzonym stosem origin/scale/rotate. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +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 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(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::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 originStack; + std::vector scaleStack; + runtime::Vec3 rotation{}; + std::size_t groupDepth = 0; +}; + +[[nodiscard]] inline runtime::Vec3 originPushDelta( + const std::vector& 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& 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 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 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& 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& 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 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 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& 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 diff --git a/eu07-parser/include/eu07/scene/binary/io.hpp b/eu07-parser/include/eu07/scene/binary/io.hpp new file mode 100644 index 00000000..7f5f5b69 --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/io.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::scene::binary::io { + +inline void writeU8(std::ostream& out, const std::uint8_t v) { + out.put(static_cast(v)); +} + +inline void writeU16(std::ostream& out, const std::uint16_t v) { + const auto b = std::array{ + static_cast(v), + static_cast(v >> 8), + }; + out.write(reinterpret_cast(b.data()), 2); +} + +inline void writeU32(std::ostream& out, const std::uint32_t v) { + const auto b = std::array{ + static_cast(v), + static_cast(v >> 8), + static_cast(v >> 16), + static_cast(v >> 24), + }; + out.write(reinterpret_cast(b.data()), 4); +} + +inline void writeU64(std::ostream& out, const std::uint64_t v) { + writeU32(out, static_cast(v)); + writeU32(out, static_cast(v >> 32)); +} + +inline void writeI32(std::ostream& out, const std::int32_t v) { + writeU32(out, static_cast(v)); +} + +inline void writeI16(std::ostream& out, const std::int16_t v) { + writeU16(out, static_cast(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(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& 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(ch); +} + +[[nodiscard]] inline std::uint32_t readU32(std::istream& in) { + std::array b{}; + in.read(reinterpret_cast(b.data()), 4); + if (!in) { + throw std::runtime_error("EU7B: nieoczekiwany koniec pliku"); + } + return static_cast(b[0]) | (static_cast(b[1]) << 8) | + (static_cast(b[2]) << 16) | (static_cast(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(readU32(in)); +} + +[[nodiscard]] inline std::uint16_t readU16(std::istream& in) { + std::array b{}; + in.read(reinterpret_cast(b.data()), 2); + if (!in) { + throw std::runtime_error("EU7B: nieoczekiwany koniec pliku"); + } + return static_cast(b[0]) | (static_cast(b[1]) << 8); +} + +[[nodiscard]] inline std::int16_t readI16(std::istream& in) { + return static_cast(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(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(len)); + if (!in) { + throw std::runtime_error("EU7B: nieoczekiwany koniec pliku (string)"); + } + } + return text; +} + +} // namespace eu07::scene::binary::io diff --git a/eu07-parser/include/eu07/scene/binary/runtime_chunks_directives.hpp b/eu07-parser/include/eu07/scene/binary/runtime_chunks_directives.hpp new file mode 100644 index 00000000..275d2cbb --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/runtime_chunks_directives.hpp @@ -0,0 +1,165 @@ +#pragma once + +// EU7B v2 — chunki TRSET, EVNT (include wewnątrz binary::detail). + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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(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(trainset.vehicleIndices.size())); + for (const std::size_t index : trainset.vehicleIndices) { + io::writeU32(out, static_cast(index)); + } + io::writeU32(out, static_cast(trainset.couplings.size())); + for (const int coupling : trainset.couplings) { + io::writeI32(out, coupling); + } + io::writeU32(out, static_cast(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(event.type)); + io::writeF64(out, event.delay); + io::writeU32(out, static_cast(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(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 buildTrsetPayloadV2( + StringTable& table, + const std::vector& trainsets) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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 buildEvntPayloadV2( + StringTable& table, + const std::vector& events) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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(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)); + } +} diff --git a/eu07-parser/include/eu07/scene/binary/runtime_chunks_sim.hpp b/eu07-parser/include/eu07/scene/binary/runtime_chunks_sim.hpp new file mode 100644 index 00000000..8132c159 --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/runtime_chunks_sim.hpp @@ -0,0 +1,429 @@ +#pragma once + +// EU7B v2 — chunki TRAC, PWRS, MEMC, LAUN, DYNM, SOND (include wewnątrz binary::detail). + +#include +#include +#include +#include + +#include +#include +#include +#include + +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(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(*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 buildTracPayloadV2( + StringTable& table, + const std::vector& traction) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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 buildMemcPayloadV2( + StringTable& table, + const std::vector& cells) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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 buildLaunPayloadV2( + StringTable& table, + const std::vector& launchers) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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 buildDynmPayloadV2( + StringTable& table, + const std::vector& dynamics) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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 buildSondPayloadV2( + StringTable& table, + const std::vector& sounds) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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(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(source.modifier)); +} + +inline void collectPowerSourceStrings( + StringTable& table, + const runtime::RuntimeTractionPowerSource& source) { + table.intern(source.node.name); +} + +[[nodiscard]] inline std::vector buildPwrsPayloadV2( + StringTable& table, + const std::vector& sources) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(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(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)); + } +} diff --git a/eu07-parser/include/eu07/scene/binary/runtime_codec.hpp b/eu07-parser/include/eu07/scene/binary/runtime_codec.hpp new file mode 100644 index 00000000..6d9b03f5 --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/runtime_codec.hpp @@ -0,0 +1,737 @@ +#pragma once + +// EU7B v5 — slim node, TERR terrain-lite, packed vertices, track tail enums. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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::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(transform.originStack.size())); + for (const runtime::Vec3& offset : transform.originStack) { + io::writeVec3(out, offset); + } + io::writeU8(out, static_cast(transform.scaleStack.size())); + for (const runtime::Vec3& scale : transform.scaleStack) { + io::writeVec3(out, scale); + } + io::writeVec3(out, transform.rotation); + io::writeU8(out, static_cast(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::lround(clamped * 32767.f)); +} + +inline float snorm16ToFloat(const std::int16_t value) { + return static_cast(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(sign | 0x7C00u | (mantissa ? 0x0200u : 0u)); + } + if (fExp == 0) { + return static_cast(sign); + } + + std::int32_t halfExp = static_cast(fExp) - 127 + 15; + if (halfExp >= 31) { + return static_cast(sign | 0x7C00u); + } + if (halfExp <= 0) { + if (halfExp < -10) { + return static_cast(sign); + } + mantissa = (mantissa | 0x800000u) >> static_cast(1 - halfExp); + return static_cast(sign | (mantissa >> 13u)); + } + + return static_cast( + sign | (static_cast(halfExp) << 10u) | ((mantissa + 0x1000u) >> 13u)); +} + +inline float halfToFloat(const std::uint16_t value) { + const std::uint32_t sign = static_cast(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(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::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(vertex.normal.x))); + io::writeI16(out, detail::floatToSnorm16(static_cast(vertex.normal.y))); + io::writeI16(out, detail::floatToSnorm16(static_cast(vertex.normal.z))); + detail::writeHalf(out, static_cast(vertex.u)); + detail::writeHalf(out, static_cast(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 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(key.x) * 0x9E3779B9u ^ + static_cast(key.z); + } +}; + +struct TerrSectionBatch { + TerrSectionKey section{}; + std::vector vertices; +}; + +[[nodiscard]] inline runtime::Vec3 terrTriangleCentroid( + const std::array& 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(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::floor( + centroid.x / static_cast(kEu07SectionSize) + + static_cast(kEu07RegionSideSectionCount) / 2.0)); + key.z = static_cast(std::floor( + centroid.z / static_cast(kEu07SectionSize) + + static_cast(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& 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 groupTerrShapesBySection( + const std::vector& shapes) { + std::unordered_map batchIndex; + std::vector 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 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 models; +}; + +[[nodiscard]] inline TerrSectionKey clampTerrSectionKey(const TerrSectionKey key) noexcept { + TerrSectionKey clamped = key; + clamped.x = std::clamp( + clamped.x, + 0, + static_cast(kEu07RegionSideSectionCount) - 1); + clamped.z = std::clamp( + clamped.z, + 0, + static_cast(kEu07RegionSideSectionCount) - 1); + return clamped; +} + +[[nodiscard]] inline std::vector groupModelsBySection( + const std::vector& models) { + std::unordered_map batchIndex; + std::vector 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 diff --git a/eu07-parser/include/eu07/scene/binary/runtime_module.hpp b/eu07-parser/include/eu07/scene/binary/runtime_module.hpp new file mode 100644 index 00000000..e9bf47ca --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/runtime_module.hpp @@ -0,0 +1,1058 @@ +#pragma once + +// EU7B runtime (version=5) — 1 plik tekstowy → 1 .eu7. +// Chunki: STRS, INCL, TRAK, TRAC, PWRS, TERR, MESH, LINE, MODL, MEMC, LAUN, DYNM, SOND, TRSET, EVNT, FINT. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::scene::binary { + +inline constexpr std::array kChunkStrs{'S', 'T', 'R', 'S'}; +inline constexpr std::array kChunkIncl{'I', 'N', 'C', 'L'}; +inline constexpr std::array kChunkTrak{'T', 'R', 'A', 'K'}; +inline constexpr std::array kChunkMesh{'M', 'E', 'S', 'H'}; +inline constexpr std::array kChunkTerr{'T', 'E', 'R', 'R'}; +inline constexpr std::array kChunkLine{'L', 'I', 'N', 'E'}; +inline constexpr std::array kChunkModl{'M', 'O', 'D', 'L'}; +inline constexpr std::array kChunkTrac{'T', 'R', 'A', 'C'}; +inline constexpr std::array kChunkPwrs{'P', 'W', 'R', 'S'}; +inline constexpr std::array kChunkMemc{'M', 'E', 'M', 'C'}; +inline constexpr std::array kChunkLaun{'L', 'A', 'U', 'N'}; +inline constexpr std::array kChunkDynm{'D', 'Y', 'N', 'M'}; +inline constexpr std::array kChunkSond{'S', 'O', 'N', 'D'}; +inline constexpr std::array kChunkTrset{'T', 'R', 'S', 'E'}; +inline constexpr std::array kChunkEvnt{'E', 'V', 'N', 'T'}; +inline constexpr std::array kChunkFint{'F', 'I', 'N', 'T'}; +inline constexpr std::array kChunkPlac{'P', 'L', 'A', 'C'}; +inline constexpr std::array kChunkPidx{'P', 'I', 'D', 'X'}; +inline constexpr std::array kChunkPack{'P', 'A', 'C', 'K'}; + +struct PackWriteStats { + std::size_t strings_total = 0; + std::size_t pack_entries = 0; + std::size_t pack_bytes = 0; + std::size_t pack_build_ms = 0; + std::size_t write_file_ms = 0; + unsigned pack_workers = 0; +}; + +inline void printPackWriteStats(const PackWriteStats& stats, std::ostream& out) { + out << "[EU7] PACK zapis:\n" + << " strs=" << stats.strings_total << " pidx=" << stats.pack_entries + << " pack_B=" << stats.pack_bytes << '\n' + << " czas_ms: build=" << stats.pack_build_ms << " zapis_pliku=" << stats.write_file_ms + << " watki_build=" << stats.pack_workers << '\n' + << std::flush; +} + +struct WriteRuntimeModuleOptions { + // Root scenariusz: MODL → PIDX+PACK (EU7B v7) zamiast plaskiego MODL. + bool emitPackModels = false; + // Sekcje 1 km z compose — serializacja PACK po zbudowaniu STRS. + const std::vector* pack_batches = nullptr; + PackWriteStats* pack_write_stats = nullptr; +}; + +struct LoadedRuntimeModule { + bake::RuntimeModule module; + std::uint32_t version = 0; + StringTable strings; + struct ChunkSummary { + std::array id{}; + std::uint32_t size = 0; + bool recognized = false; + }; + std::vector chunks; +}; + +namespace detail { + +inline void writeSegmentPath(std::ostream& out, const runtime::SegmentPath& seg) { + io::writeVec3(out, seg.pStart); + io::writeF64(out, seg.rollStart); + io::writeVec3(out, seg.cpOut); + io::writeVec3(out, seg.cpIn); + io::writeVec3(out, seg.pEnd); + io::writeF64(out, seg.rollEnd); + io::writeF64(out, seg.radius); +} + +inline void writeTrackVisibility( + std::ostream& out, + StringTable& table, + const runtime::TrackVisibility& vis) { + codec::writeStringId(out, table, vis.material1); + io::writeF32(out, vis.texLength); + codec::writeStringId(out, table, vis.material2); + io::writeF32(out, vis.texHeight1); + io::writeF32(out, vis.texWidth); + io::writeF32(out, vis.texSlope); +} + +inline void writeRuntimeTrack(std::ostream& out, StringTable& table, const runtime::RuntimeTrack& track) { + codec::writeSlimNode(out, table, track.node, "track"); + io::writeU8(out, static_cast(track.trackType)); + io::writeU8(out, static_cast(track.category)); + io::writeF32(out, track.length); + io::writeF32(out, track.trackWidth); + io::writeF32(out, track.friction); + io::writeF32(out, track.soundDistance); + io::writeI32(out, track.qualityFlag); + io::writeI32(out, track.damageFlag); + io::writeU8(out, static_cast(static_cast(track.environment) + 1)); + io::writeU8(out, track.visibility.has_value() ? 1 : 0); + if (track.visibility) { + writeTrackVisibility(out, table, *track.visibility); + } + io::writeU32(out, static_cast(track.paths.size())); + for (const runtime::SegmentPath& seg : track.paths) { + writeSegmentPath(out, seg); + } + io::writeU32(out, static_cast(track.tailKeywords.size())); + for (const auto& [key, value] : track.tailKeywords) { + codec::writeTrackTailEntry(out, table, key, value); + } +} + +inline void writeRuntimeShape(std::ostream& out, StringTable& table, const runtime::RuntimeShapeNode& shape) { + const std::uint8_t subtype = codec::meshSubtypeCode( + shape.node.nodeType.empty() ? std::string_view{"triangles"} : shape.node.nodeType); + io::writeU8(out, subtype); + codec::writeSlimNode(out, table, shape.node, codec::meshSubtypeName(subtype)); + io::writeU8(out, shape.translucent ? 1 : 0); + codec::writeStringId(out, table, shape.materialPath); + codec::writeLightingTagged(out, shape.lighting); + io::writeVec3(out, shape.origin); + io::writeU32(out, static_cast(shape.vertices.size())); + for (const runtime::WorldVertex& vertex : shape.vertices) { + codec::writePackedWorldVertex(out, vertex); + } +} + +inline void writeRuntimeLines(std::ostream& out, StringTable& table, const runtime::RuntimeLinesNode& lines) { + const std::uint8_t subtype = codec::lineSubtypeCode( + lines.node.nodeType.empty() ? std::string_view{"lines"} : lines.node.nodeType); + io::writeU8(out, subtype); + codec::writeSlimNode(out, table, lines.node, codec::lineSubtypeName(subtype)); + codec::writeLightingTagged(out, lines.lighting); + io::writeF32(out, lines.lineWidth); + io::writeVec3(out, lines.origin); + io::writeU32(out, static_cast(lines.vertices.size())); + for (const runtime::WorldVertex& vertex : lines.vertices) { + io::writeVec3(out, vertex.position); + } +} + +inline void writeRuntimeModel(std::ostream& out, StringTable& table, const runtime::RuntimeModelInstance& model) { + codec::writeSlimNode(out, table, model.node, "model"); + io::writeU8(out, model.isTerrain ? 1 : 0); + io::writeU8(out, model.transition ? 1 : 0); + io::writeVec3(out, model.location); + io::writeVec3(out, model.angles); + io::writeVec3(out, model.scale); + codec::writeStringId(out, table, model.modelFile); + codec::writeStringId(out, table, model.textureFile); + io::writeU32(out, static_cast(model.lightStates.size())); + for (float light : model.lightStates) { + io::writeF32(out, light); + } + io::writeU32(out, static_cast(model.lightColors.size())); + for (std::uint32_t color : model.lightColors) { + io::writeU32(out, color); + } +} + +inline void collectShapeStrings(StringTable& table, const runtime::RuntimeShapeNode& shape) { + table.intern(shape.node.name); + table.intern(shape.materialPath); +} + +inline void collectLinesStrings(StringTable& table, const runtime::RuntimeLinesNode& lines) { + table.intern(lines.node.name); +} + +inline void collectModelStrings(StringTable& table, const runtime::RuntimeModelInstance& model) { + table.intern(model.node.name); + table.intern(model.modelFile); + table.intern(model.textureFile); +} + +inline void collectTrackStrings(StringTable& table, const runtime::RuntimeTrack& track) { + table.intern(track.node.name); + if (track.visibility) { + table.intern(track.visibility->material1); + table.intern(track.visibility->material2); + } + for (const auto& [key, value] : track.tailKeywords) { + if (codec::trackTailKeywordCode(key) == 255) { + table.intern(key); + } + table.intern(value); + } +} + +#include +#include + +inline void collectModuleStrings(StringTable& table, const bake::RuntimeModule& module) { + for (const bake::ModuleInclude& inc : module.includes) { + table.intern(inc.sourcePath); + table.intern(inc.binaryPath); + for (const std::string& param : inc.parameters) { + table.intern(param); + } + } + for (const runtime::RuntimeTrack& track : module.scene.tracks) { + collectTrackStrings(table, track); + } + for (const runtime::RuntimeShapeNode& shape : module.scene.shapes) { + collectShapeStrings(table, shape); + } + for (const runtime::RuntimeLinesNode& lines : module.scene.lines) { + collectLinesStrings(table, lines); + } + for (const runtime::RuntimeModelInstance& model : module.scene.models) { + collectModelStrings(table, model); + } + for (const runtime::RuntimeTraction& traction : module.scene.traction) { + collectTractionStrings(table, traction); + } + for (const runtime::RuntimeTractionPowerSource& source : module.scene.powerSources) { + collectPowerSourceStrings(table, source); + } + for (const runtime::RuntimeMemCell& cell : module.scene.memcells) { + collectMemcellStrings(table, cell); + } + for (const runtime::RuntimeEventLauncher& launcher : module.scene.eventLaunchers) { + collectLauncherStrings(table, launcher); + } + for (const runtime::RuntimeDynamicObject& vehicle : module.scene.dynamics) { + collectDynamicStrings(table, vehicle); + } + for (const runtime::RuntimeSoundSource& sound : module.scene.sounds) { + collectSoundStrings(table, sound); + } + for (const runtime::RuntimeTrainset& trainset : module.scene.trainsets) { + collectTrainsetStrings(table, trainset); + } + for (const runtime::RuntimeEvent& event : module.scene.events) { + collectEventStrings(table, event); + } +} + +[[nodiscard]] inline std::vector buildStrsPayload(const StringTable& table) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(table.strings().size())); + for (const std::string& text : table.strings()) { + io::writeU32(out, static_cast(text.size())); + out.write(text.data(), static_cast(text.size())); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildInclPayloadV2( + StringTable& table, + const std::vector& includes) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(includes.size())); + for (const bake::ModuleInclude& inc : includes) { + io::writeU32(out, inc.sourceLine); + codec::writeStringId(out, table, inc.sourcePath); + codec::writeStringId(out, table, inc.binaryPath); + io::writeU32(out, static_cast(inc.parameters.size())); + for (const std::string& param : inc.parameters) { + codec::writeStringId(out, table, param); + } + codec::detail::writeTransformContext(out, inc.siteTransform); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildTrakPayloadV2( + StringTable& table, + const std::vector& tracks) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(tracks.size())); + for (const runtime::RuntimeTrack& track : tracks) { + writeRuntimeTrack(out, table, track); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildMeshPayloadV2( + StringTable& table, + const std::vector& shapes) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(shapes.size())); + for (const runtime::RuntimeShapeNode& shape : shapes) { + writeRuntimeShape(out, table, shape); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildTerrPayloadV2( + StringTable& table, + const std::vector& shapes) { + const std::uint8_t flags = codec::terrFlagsForShape(shapes.front()); + std::ostringstream out(std::ios::binary); + io::writeU8(out, flags); + codec::writeStringId(out, table, shapes.front().materialPath); + if ((flags & codec::kTerrFlagNonDefaultLighting) != 0) { + codec::writeLightingBlock(out, shapes.front().lighting); + } + io::writeU32(out, static_cast(shapes.size())); + for (const runtime::RuntimeShapeNode& shape : shapes) { + if (shape.vertices.size() != codec::kTerrVertsPerRecord) { + throw std::runtime_error("EU7B TERR: oczekiwano 3 wierzcholkow na rekord"); + } + for (const runtime::WorldVertex& vertex : shape.vertices) { + codec::writePackedWorldVertex(out, vertex); + } + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildTerrBatchedPayloadV2( + StringTable& table, + const std::vector& shapes) { + const std::vector batches = codec::groupTerrShapesBySection(shapes); + const std::uint8_t flags = codec::terrFlagsForShape(shapes.front(), true); + std::ostringstream out(std::ios::binary); + io::writeU8(out, flags); + codec::writeStringId(out, table, shapes.front().materialPath); + if ((flags & codec::kTerrFlagNonDefaultLighting) != 0) { + codec::writeLightingBlock(out, shapes.front().lighting); + } + io::writeU32(out, static_cast(batches.size())); + for (const codec::TerrSectionBatch& batch : batches) { + io::writeI32(out, batch.section.x); + io::writeI32(out, batch.section.z); + io::writeU32(out, static_cast(batch.vertices.size())); + for (const runtime::WorldVertex& vertex : batch.vertices) { + codec::writePackedWorldVertex(out, vertex); + } + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildLinePayloadV2( + StringTable& table, + const std::vector& lines) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(lines.size())); + for (const runtime::RuntimeLinesNode& line : lines) { + writeRuntimeLines(out, table, line); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline std::vector buildModlPayloadV2( + StringTable& table, + const std::vector& models) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(models.size())); + for (const runtime::RuntimeModelInstance& model : models) { + writeRuntimeModel(out, table, model); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +struct PackSectionIndexEntry { + std::uint16_t row = 0; + std::uint16_t column = 0; + std::uint32_t modelCount = 0; + std::uint64_t packOffset = 0; +}; + +struct PackPayloadBuild { + std::vector packPayload; + std::vector entries; +}; + +[[nodiscard]] inline std::vector buildPackSectionPayload( + StringTable& table, + const std::vector& models) { + std::ostringstream packOut(std::ios::binary); + for (const runtime::RuntimeModelInstance& model : models) { + runtime::RuntimeModelInstance worldModel = model; + worldModel.node.transform = {}; + writeRuntimeModel(packOut, table, worldModel); + } + const std::string blob = packOut.str(); + return {blob.begin(), blob.end()}; +} + +[[nodiscard]] inline PackPayloadBuild buildPackPayloadV7( + StringTable& table, + const std::vector& batches) { + PackPayloadBuild result; + if (batches.empty()) { + return result; + } + + for (const codec::ModelSectionBatch& batch : batches) { + for (const runtime::RuntimeModelInstance& model : batch.models) { + collectModelStrings(table, model); + } + } + + result.entries.resize(batches.size()); + std::vector> section_payloads(batches.size()); + + const unsigned hw = + std::thread::hardware_concurrency() == 0 ? 4u : std::thread::hardware_concurrency(); + const unsigned worker_count = std::max(1u, std::min(hw, static_cast(batches.size()))); + + if (worker_count == 1 || batches.size() < 8) { + for (std::size_t batch_index = 0; batch_index < batches.size(); ++batch_index) { + const codec::ModelSectionBatch& batch = batches[batch_index]; + PackSectionIndexEntry& entry = result.entries[batch_index]; + entry.row = static_cast(batch.section.z); + entry.column = static_cast(batch.section.x); + entry.modelCount = static_cast(batch.models.size()); + section_payloads[batch_index] = buildPackSectionPayload(table, batch.models); + } + std::size_t total_size = 0; + for (const std::vector& payload : section_payloads) { + total_size += payload.size(); + } + result.packPayload.clear(); + result.packPayload.reserve(total_size); + std::uint64_t offset = 0; + for (std::size_t batch_index = 0; batch_index < batches.size(); ++batch_index) { + result.entries[batch_index].packOffset = offset; + const std::vector& payload = section_payloads[batch_index]; + result.packPayload.insert(result.packPayload.end(), payload.begin(), payload.end()); + offset += static_cast(payload.size()); + } + return result; + } + + std::atomic next_batch { 0 }; + std::vector workers; + workers.reserve(worker_count); + for (unsigned worker = 0; worker < worker_count; ++worker) { + workers.emplace_back([&]() { + while (true) { + const std::size_t batch_index = next_batch.fetch_add(1, std::memory_order_relaxed); + if (batch_index >= batches.size()) { + return; + } + const codec::ModelSectionBatch& batch = batches[batch_index]; + PackSectionIndexEntry& entry = result.entries[batch_index]; + entry.row = static_cast(batch.section.z); + entry.column = static_cast(batch.section.x); + entry.modelCount = static_cast(batch.models.size()); + + section_payloads[batch_index] = buildPackSectionPayload(table, batch.models); + } + }); + } + for (std::thread& worker : workers) { + if (worker.joinable()) { + worker.join(); + } + } + + std::size_t total_size = 0; + for (const std::vector& payload : section_payloads) { + total_size += payload.size(); + } + result.packPayload.clear(); + result.packPayload.reserve(total_size); + std::uint64_t offset = 0; + for (std::size_t batch_index = 0; batch_index < batches.size(); ++batch_index) { + result.entries[batch_index].packOffset = offset; + const std::vector& payload = section_payloads[batch_index]; + result.packPayload.insert(result.packPayload.end(), payload.begin(), payload.end()); + offset += static_cast(payload.size()); + } + return result; +} + +[[nodiscard]] inline PackPayloadBuild buildPackPayloadV7( + StringTable& table, + const std::vector& models) { + return buildPackPayloadV7(table, codec::groupModelsBySection(models)); +} + +} // namespace detail + +namespace detail { + +[[nodiscard]] inline std::vector buildPidxPayloadV7( + const std::vector& entries) { + std::ostringstream out(std::ios::binary); + io::writeU32(out, static_cast(entries.size())); + for (const PackSectionIndexEntry& entry : entries) { + io::writeU16(out, entry.row); + io::writeU16(out, entry.column); + io::writeU32(out, entry.modelCount); + io::writeU64(out, entry.packOffset); + } + const std::string blob = out.str(); + return {blob.begin(), blob.end()}; +} + +inline runtime::SegmentPath readSegmentPath(std::istream& in) { + runtime::SegmentPath seg; + seg.pStart = io::readVec3(in); + seg.rollStart = io::readF64(in); + seg.cpOut = io::readVec3(in); + seg.cpIn = io::readVec3(in); + seg.pEnd = io::readVec3(in); + seg.rollEnd = io::readF64(in); + seg.radius = io::readF64(in); + return seg; +} + +inline runtime::TrackVisibility readTrackVisibility(std::istream& in, const StringTable& table) { + runtime::TrackVisibility vis; + vis.material1 = table.resolve(io::readU32(in)); + vis.texLength = io::readF32(in); + vis.material2 = table.resolve(io::readU32(in)); + vis.texHeight1 = io::readF32(in); + vis.texWidth = io::readF32(in); + vis.texSlope = io::readF32(in); + return vis; +} + +inline runtime::RuntimeTrack readRuntimeTrack(std::istream& in, const StringTable& table) { + runtime::RuntimeTrack track; + track.node = codec::readSlimNode(in, table, "track"); + track.trackType = static_cast(io::readU8(in)); + track.category = static_cast(io::readU8(in)); + track.length = io::readF32(in); + track.trackWidth = io::readF32(in); + track.friction = io::readF32(in); + track.soundDistance = io::readF32(in); + track.qualityFlag = io::readI32(in); + track.damageFlag = io::readI32(in); + track.environment = static_cast(static_cast(io::readU8(in)) - 1); + if (io::readU8(in) != 0) { + track.visibility = readTrackVisibility(in, table); + } + const std::uint32_t pathCount = io::readU32(in); + track.paths.reserve(pathCount); + for (std::uint32_t i = 0; i < pathCount; ++i) { + track.paths.push_back(readSegmentPath(in)); + } + const std::uint32_t tailCount = io::readU32(in); + track.tailKeywords.reserve(tailCount); + for (std::uint32_t i = 0; i < tailCount; ++i) { + track.tailKeywords.push_back(codec::readTrackTailEntry(in, table)); + } + return track; +} + +inline void readStrsChunk(std::istream& in, StringTable& table) { + const std::uint32_t count = io::readU32(in); + std::vector strings; + strings.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + strings.push_back(io::readString(in)); + } + table.load(std::move(strings)); +} + +inline void readInclChunkV2( + std::istream& in, + const StringTable& table, + const std::uint32_t fileVersion, + bake::RuntimeModule& module) { + const std::uint32_t count = io::readU32(in); + module.includes.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + bake::ModuleInclude inc; + inc.sourceLine = io::readU32(in); + inc.sourcePath = table.resolve(io::readU32(in)); + inc.binaryPath = table.resolve(io::readU32(in)); + const std::uint32_t paramCount = io::readU32(in); + inc.parameters.reserve(paramCount); + for (std::uint32_t p = 0; p < paramCount; ++p) { + inc.parameters.push_back(table.resolve(io::readU32(in))); + } + if (fileVersion >= kVersionRuntime) { + inc.siteTransform = codec::detail::readTransformContext(in); + } + module.includes.push_back(std::move(inc)); + } +} + +inline void readTrakChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) { + const std::uint32_t count = io::readU32(in); + module.scene.tracks.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + module.scene.tracks.push_back(readRuntimeTrack(in, table)); + } +} + +inline runtime::RuntimeShapeNode readRuntimeShape(std::istream& in, const StringTable& table) { + runtime::RuntimeShapeNode shape; + const std::uint8_t subtype = io::readU8(in); + shape.node = codec::readSlimNode(in, table, codec::meshSubtypeName(subtype)); + shape.translucent = io::readU8(in) != 0; + shape.materialPath = table.resolve(io::readU32(in)); + shape.lighting = codec::readLightingTagged(in); + shape.origin = io::readVec3(in); + const std::uint32_t vertexCount = io::readU32(in); + shape.vertices.reserve(vertexCount); + for (std::uint32_t i = 0; i < vertexCount; ++i) { + shape.vertices.push_back(codec::readPackedWorldVertex(in)); + } + return shape; +} + +inline runtime::RuntimeLinesNode readRuntimeLines(std::istream& in, const StringTable& table) { + runtime::RuntimeLinesNode lines; + const std::uint8_t subtype = io::readU8(in); + lines.node = codec::readSlimNode(in, table, codec::lineSubtypeName(subtype)); + lines.lighting = codec::readLightingTagged(in); + lines.lineWidth = io::readF32(in); + lines.origin = io::readVec3(in); + const std::uint32_t vertexCount = io::readU32(in); + lines.vertices.reserve(vertexCount); + for (std::uint32_t i = 0; i < vertexCount; ++i) { + runtime::WorldVertex vertex; + vertex.position = io::readVec3(in); + lines.vertices.push_back(vertex); + } + return lines; +} + +inline runtime::RuntimeModelInstance readRuntimeModel(std::istream& in, const StringTable& table) { + runtime::RuntimeModelInstance model; + model.node = codec::readSlimNode(in, table, "model"); + model.isTerrain = io::readU8(in) != 0; + model.transition = io::readU8(in) != 0; + model.location = io::readVec3(in); + model.angles = io::readVec3(in); + model.scale = io::readVec3(in); + model.modelFile = table.resolve(io::readU32(in)); + model.textureFile = table.resolve(io::readU32(in)); + const std::uint32_t lightCount = io::readU32(in); + model.lightStates.resize(lightCount); + for (std::uint32_t i = 0; i < lightCount; ++i) { + model.lightStates[i] = io::readF32(in); + } + const std::uint32_t colorCount = io::readU32(in); + model.lightColors.resize(colorCount); + for (std::uint32_t i = 0; i < colorCount; ++i) { + model.lightColors[i] = io::readU32(in); + } + return model; +} + +inline void readMeshChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) { + const std::uint32_t count = io::readU32(in); + module.scene.shapes.reserve(module.scene.shapes.size() + count); + for (std::uint32_t i = 0; i < count; ++i) { + module.scene.shapes.push_back(readRuntimeShape(in, table)); + } +} + +inline runtime::RuntimeShapeNode makeTerrShape( + const std::string& material, + const std::uint8_t flags, + const runtime::LightingData& lighting) { + runtime::RuntimeShapeNode shape; + shape.node.nodeType = "triangles"; + shape.node.rangeSquaredMin = 0.0; + shape.node.rangeSquaredMax = std::numeric_limits::max(); + shape.node.visible = true; + shape.materialPath = material; + shape.translucent = (flags & codec::kTerrFlagTranslucent) != 0; + shape.lighting = lighting; + shape.origin = {}; + return shape; +} + +inline void readTerrChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) { + const std::uint8_t flags = io::readU8(in); + const std::string material = table.resolve(io::readU32(in)); + runtime::LightingData lighting; + if ((flags & codec::kTerrFlagNonDefaultLighting) != 0) { + lighting = codec::readLightingBlock(in); + } + + if ((flags & codec::kTerrFlagBatched) != 0) { + const std::uint32_t batchCount = io::readU32(in); + module.scene.shapes.reserve(module.scene.shapes.size() + batchCount); + for (std::uint32_t i = 0; i < batchCount; ++i) { + (void)io::readI32(in); + (void)io::readI32(in); + runtime::RuntimeShapeNode shape = makeTerrShape(material, flags, lighting); + const std::uint32_t vertexCount = io::readU32(in); + if (vertexCount % codec::kTerrVertsPerRecord != 0) { + throw std::runtime_error("EU7B TERR: liczba wierzcholkow w batchu musi byc wielokrotnoscia 3"); + } + shape.vertices.resize(vertexCount); + for (std::uint32_t v = 0; v < vertexCount; ++v) { + shape.vertices[v] = codec::readPackedWorldVertex(in); + } + bake::computeBounds(shape); + module.scene.shapes.push_back(std::move(shape)); + } + return; + } + + const std::uint32_t count = io::readU32(in); + module.scene.shapes.reserve(module.scene.shapes.size() + count); + for (std::uint32_t i = 0; i < count; ++i) { + runtime::RuntimeShapeNode shape = makeTerrShape(material, flags, lighting); + shape.vertices.resize(codec::kTerrVertsPerRecord); + for (std::size_t v = 0; v < codec::kTerrVertsPerRecord; ++v) { + shape.vertices[v] = codec::readPackedWorldVertex(in); + } + bake::computeBounds(shape); + module.scene.shapes.push_back(std::move(shape)); + } +} + +inline void readLineChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) { + const std::uint32_t count = io::readU32(in); + module.scene.lines.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + module.scene.lines.push_back(readRuntimeLines(in, table)); + } +} + +inline void readModlChunkV2(std::istream& in, const StringTable& table, bake::RuntimeModule& module) { + const std::uint32_t count = io::readU32(in); + module.scene.models.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + module.scene.models.push_back(readRuntimeModel(in, table)); + } +} + +inline void readPlacChunk(std::istream& in, bake::RuntimeModule& module) { + module.includePlacement.origin_x_param = io::readU8(in); + module.includePlacement.origin_y_param = io::readU8(in); + module.includePlacement.origin_z_param = io::readU8(in); + module.includePlacement.rotation_y_param = io::readU8(in); +} + +inline void readPidxChunkV7(std::istream& in, std::vector& entries) { + const std::uint32_t count = io::readU32(in); + entries.clear(); + entries.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + PackSectionIndexEntry entry; + entry.row = io::readU16(in); + entry.column = io::readU16(in); + entry.modelCount = io::readU32(in); + entry.packOffset = io::readU64(in); + entries.push_back(entry); + } +} + +[[nodiscard]] inline bool isKnownRuntimeChunk(const std::array& id) { + return id == kChunkStrs || id == kChunkIncl || id == kChunkPlac || id == kChunkTrak || + id == kChunkTrac || id == kChunkPwrs || id == kChunkTerr || id == kChunkMesh || + id == kChunkLine || id == kChunkModl || id == kChunkPidx || id == kChunkPack || id == kChunkMemc || + id == kChunkLaun || id == kChunkDynm || id == kChunkSond || + id == kChunkTrset || id == kChunkEvnt || id == kChunkFint; +} + +} // namespace detail + +inline void writeRuntimeModule( + const std::filesystem::path& outPath, + const bake::RuntimeModule& module, + WriteRuntimeModuleOptions options = {}) { + const auto write_begin = std::chrono::steady_clock::now(); + StringTable table; + detail::collectModuleStrings(table, module); + + const bool usePackModels = options.emitPackModels && + (options.pack_batches != nullptr || !module.scene.models.empty()); + + detail::PackPayloadBuild packBuild; + if (usePackModels) { + const auto pack_begin = std::chrono::steady_clock::now(); + if (options.pack_batches != nullptr) { + packBuild = detail::buildPackPayloadV7(table, *options.pack_batches); + } else { + packBuild = detail::buildPackPayloadV7(table, module.scene.models); + } + if (options.pack_write_stats != nullptr) { + const auto pack_end = std::chrono::steady_clock::now(); + options.pack_write_stats->pack_build_ms = static_cast( + std::chrono::duration_cast(pack_end - pack_begin).count()); + options.pack_write_stats->pack_entries = packBuild.entries.size(); + options.pack_write_stats->pack_bytes = packBuild.packPayload.size(); + options.pack_write_stats->strings_total = table.strings().size(); + if (options.pack_batches != nullptr) { + const unsigned hw = std::thread::hardware_concurrency() == 0 + ? 4u + : std::thread::hardware_concurrency(); + options.pack_write_stats->pack_workers = std::max( + 1u, + std::min( + hw, static_cast(options.pack_batches->size()))); + } + } + } + + const std::vector strsPayload = detail::buildStrsPayload(table); + const std::vector inclPayload = detail::buildInclPayloadV2(table, module.includes); + const std::vector trakPayload = detail::buildTrakPayloadV2(table, module.scene.tracks); + const bool useTerr = codec::canUseTerrEncoding(module.scene.shapes); + const std::vector terrPayload = + useTerr ? detail::buildTerrBatchedPayloadV2(table, module.scene.shapes) : std::vector{}; + const std::vector meshPayload = useTerr ? std::vector{} + : detail::buildMeshPayloadV2(table, module.scene.shapes); + const std::vector linePayload = detail::buildLinePayloadV2(table, module.scene.lines); + const std::vector modlPayload = + usePackModels ? std::vector{} + : detail::buildModlPayloadV2(table, module.scene.models); + const std::vector pidxPayload = + usePackModels ? detail::buildPidxPayloadV7(packBuild.entries) : std::vector{}; + const std::vector tracPayload = detail::buildTracPayloadV2(table, module.scene.traction); + const std::vector pwrsPayload = + detail::buildPwrsPayloadV2(table, module.scene.powerSources); + const std::vector memcPayload = detail::buildMemcPayloadV2(table, module.scene.memcells); + const std::vector launPayload = + detail::buildLaunPayloadV2(table, module.scene.eventLaunchers); + const std::vector dynmPayload = detail::buildDynmPayloadV2(table, module.scene.dynamics); + const std::vector sondPayload = detail::buildSondPayloadV2(table, module.scene.sounds); + const std::vector trsetPayload = + detail::buildTrsetPayloadV2(table, module.scene.trainsets); + const std::vector evntPayload = detail::buildEvntPayloadV2(table, module.scene.events); + + 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, usePackModels ? kVersionRuntimeV7 : kVersionRuntime); + + io::writeChunkHeader(out, kChunkStrs, 8 + static_cast(strsPayload.size())); + out.write(strsPayload.data(), static_cast(strsPayload.size())); + + io::writeChunkHeader(out, kChunkIncl, 8 + static_cast(inclPayload.size())); + out.write(inclPayload.data(), static_cast(inclPayload.size())); + + if (!module.includePlacement.empty()) { + io::writeChunkHeader(out, kChunkPlac, 12); + io::writeU8(out, module.includePlacement.origin_x_param); + io::writeU8(out, module.includePlacement.origin_y_param); + io::writeU8(out, module.includePlacement.origin_z_param); + io::writeU8(out, module.includePlacement.rotation_y_param); + } + + io::writeChunkHeader(out, kChunkTrak, 8 + static_cast(trakPayload.size())); + out.write(trakPayload.data(), static_cast(trakPayload.size())); + + if (!terrPayload.empty()) { + io::writeChunkHeader(out, kChunkTerr, 8 + static_cast(terrPayload.size())); + out.write(terrPayload.data(), static_cast(terrPayload.size())); + } else if (!meshPayload.empty()) { + io::writeChunkHeader(out, kChunkMesh, 8 + static_cast(meshPayload.size())); + out.write(meshPayload.data(), static_cast(meshPayload.size())); + } + if (!module.scene.lines.empty()) { + io::writeChunkHeader(out, kChunkLine, 8 + static_cast(linePayload.size())); + out.write(linePayload.data(), static_cast(linePayload.size())); + } + if (usePackModels) { + io::writeChunkHeader(out, kChunkPidx, 8 + static_cast(pidxPayload.size())); + out.write(pidxPayload.data(), static_cast(pidxPayload.size())); + io::writeChunkHeader( + out, kChunkPack, 8 + static_cast(packBuild.packPayload.size())); + out.write( + packBuild.packPayload.data(), + static_cast(packBuild.packPayload.size())); + } else if (!module.scene.models.empty()) { + io::writeChunkHeader(out, kChunkModl, 8 + static_cast(modlPayload.size())); + out.write(modlPayload.data(), static_cast(modlPayload.size())); + } + if (!module.scene.traction.empty()) { + io::writeChunkHeader(out, kChunkTrac, 8 + static_cast(tracPayload.size())); + out.write(tracPayload.data(), static_cast(tracPayload.size())); + } + if (!module.scene.powerSources.empty()) { + io::writeChunkHeader(out, kChunkPwrs, 8 + static_cast(pwrsPayload.size())); + out.write(pwrsPayload.data(), static_cast(pwrsPayload.size())); + } + if (!module.scene.memcells.empty()) { + io::writeChunkHeader(out, kChunkMemc, 8 + static_cast(memcPayload.size())); + out.write(memcPayload.data(), static_cast(memcPayload.size())); + } + if (!module.scene.eventLaunchers.empty()) { + io::writeChunkHeader(out, kChunkLaun, 8 + static_cast(launPayload.size())); + out.write(launPayload.data(), static_cast(launPayload.size())); + } + if (!module.scene.dynamics.empty()) { + io::writeChunkHeader(out, kChunkDynm, 8 + static_cast(dynmPayload.size())); + out.write(dynmPayload.data(), static_cast(dynmPayload.size())); + } + if (!module.scene.sounds.empty()) { + io::writeChunkHeader(out, kChunkSond, 8 + static_cast(sondPayload.size())); + out.write(sondPayload.data(), static_cast(sondPayload.size())); + } + if (!module.scene.trainsets.empty()) { + io::writeChunkHeader(out, kChunkTrset, 8 + static_cast(trsetPayload.size())); + out.write(trsetPayload.data(), static_cast(trsetPayload.size())); + } + if (!module.scene.events.empty()) { + io::writeChunkHeader(out, kChunkEvnt, 8 + static_cast(evntPayload.size())); + out.write(evntPayload.data(), static_cast(evntPayload.size())); + } + if (module.scene.firstInitCount > 0) { + io::writeChunkHeader(out, kChunkFint, 12); + io::writeU32(out, module.scene.firstInitCount); + } + + if (options.pack_write_stats != nullptr) { + const auto write_end = std::chrono::steady_clock::now(); + options.pack_write_stats->write_file_ms = static_cast( + std::chrono::duration_cast(write_end - write_begin).count()); + } +} + +[[nodiscard]] inline LoadedRuntimeModule readRuntimeModule(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 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 (!isSupportedEu7bVersion(version)) { + throw std::runtime_error( + "EU7B: nieobslugiwana wersja runtime " + std::to_string(version) + + " (aktualna " + std::to_string(kVersionRuntime) + ")"); + } + + LoadedRuntimeModule loaded; + loaded.version = version; + loaded.module = bake::RuntimeModule{}; + + while (in.peek() != EOF) { + std::array 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; + + LoadedRuntimeModule::ChunkSummary summary; + summary.id = chunkId; + summary.size = chunkSize; + + const auto chunkStart = in.tellg(); + + if (chunkId == kChunkStrs) { + summary.recognized = true; + detail::readStrsChunk(in, loaded.strings); + } else if (chunkId == kChunkIncl) { + summary.recognized = true; + detail::readInclChunkV2(in, loaded.strings, loaded.version, loaded.module); + } else if (chunkId == kChunkPlac) { + summary.recognized = true; + detail::readPlacChunk(in, loaded.module); + } else if (chunkId == kChunkTrak) { + summary.recognized = true; + detail::readTrakChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkTerr) { + summary.recognized = true; + detail::readTerrChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkMesh) { + summary.recognized = true; + detail::readMeshChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkLine) { + summary.recognized = true; + detail::readLineChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkModl) { + summary.recognized = true; + detail::readModlChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkPidx) { + summary.recognized = true; + std::vector packIndex; + detail::readPidxChunkV7(in, packIndex); + } else if (chunkId == kChunkPack) { + summary.recognized = true; + in.seekg(static_cast(payloadSize), std::ios::cur); + } else if (chunkId == kChunkTrac) { + summary.recognized = true; + detail::readTracChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkPwrs) { + summary.recognized = true; + detail::readPwrsChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkMemc) { + summary.recognized = true; + detail::readMemcChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkLaun) { + summary.recognized = true; + detail::readLaunChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkDynm) { + summary.recognized = true; + detail::readDynmChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkSond) { + summary.recognized = true; + detail::readSondChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkTrset) { + summary.recognized = true; + detail::readTrsetChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkEvnt) { + summary.recognized = true; + detail::readEvntChunkV2(in, loaded.strings, loaded.module); + } else if (chunkId == kChunkFint) { + summary.recognized = true; + loaded.module.scene.firstInitCount = io::readU32(in); + } else { + in.seekg(static_cast(payloadSize), std::ios::cur); + } + + if (detail::isKnownRuntimeChunk(chunkId)) { + if (in.tellg() - chunkStart != static_cast(payloadSize)) { + throw std::runtime_error("EU7B: uszkodzony chunk payload"); + } + } + + loaded.chunks.push_back(summary); + } + + return loaded; +} + +} // namespace eu07::scene::binary diff --git a/eu07-parser/include/eu07/scene/binary/selftest.hpp b/eu07-parser/include/eu07/scene/binary/selftest.hpp new file mode 100644 index 00000000..86fb6522 --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/selftest.hpp @@ -0,0 +1,133 @@ +#pragma once + +// Self-test: RuntimeModule → tokeny → processScene → bake → porównanie liczników. + +#include +#include +#include + +#include +#include +#include + +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 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 diff --git a/eu07-parser/include/eu07/scene/binary/string_table.hpp b/eu07-parser/include/eu07/scene/binary/string_table.hpp new file mode 100644 index 00000000..1f71b99c --- /dev/null +++ b/eu07-parser/include/eu07/scene/binary/string_table.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +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(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& strings() const noexcept { + return strings_; + } + + void load(std::vector 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 mutex_ { std::make_unique() }; + std::vector strings_; + std::unordered_map index_; +}; + +} // namespace eu07::scene::binary diff --git a/eu07-parser/include/eu07/scene/boundaries.hpp b/eu07-parser/include/eu07/scene/boundaries.hpp new file mode 100644 index 00000000..97753461 --- /dev/null +++ b/eu07-parser/include/eu07/scene/boundaries.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +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 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 diff --git a/eu07-parser/include/eu07/scene/camera.hpp b/eu07-parser/include/eu07/scene/camera.hpp new file mode 100644 index 00000000..24bee942 --- /dev/null +++ b/eu07-parser/include/eu07/scene/camera.hpp @@ -0,0 +1,35 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_camera +// camera position rotation index endcamera + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/config.hpp b/eu07-parser/include/eu07/scene/config.hpp new file mode 100644 index 00000000..47e5d116 --- /dev/null +++ b/eu07-parser/include/eu07/scene/config.hpp @@ -0,0 +1,35 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_config +// config settings endconfig + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/context.hpp b/eu07-parser/include/eu07/scene/context.hpp new file mode 100644 index 00000000..88ff1417 --- /dev/null +++ b/eu07-parser/include/eu07/scene/context.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace eu07::scene { + +struct ParseContext { + SceneDocument& document; + SceneScratchpad scratch; + std::filesystem::path baseDirectory; + std::vector includeStack; + std::vector> 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 parameters; +}; + +void expandInclude(ParseContext& context, const IncludeExpansionRequest& request); + +} // namespace detail + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/cursor.hpp b/eu07-parser/include/eu07/scene/cursor.hpp new file mode 100644 index 00000000..bca28442 --- /dev/null +++ b/eu07-parser/include/eu07/scene/cursor.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace eu07::scene { + +class TokenStream { +public: + explicit TokenStream(const std::vector& 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* tokens_; + std::size_t index_ = 0; +}; + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/description.hpp b/eu07-parser/include/eu07/scene/description.hpp new file mode 100644 index 00000000..9b3ae16b --- /dev/null +++ b/eu07-parser/include/eu07/scene/description.hpp @@ -0,0 +1,35 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_description +// description text enddescription + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/detail/boundary.hpp b/eu07-parser/include/eu07/scene/detail/boundary.hpp new file mode 100644 index 00000000..e63023dc --- /dev/null +++ b/eu07-parser/include/eu07/scene/detail/boundary.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +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 diff --git a/eu07-parser/include/eu07/scene/detail/parse.hpp b/eu07-parser/include/eu07/scene/detail/parse.hpp new file mode 100644 index 00000000..1b49f3dd --- /dev/null +++ b/eu07-parser/include/eu07/scene/detail/parse.hpp @@ -0,0 +1,70 @@ +#pragma once + +// Pomocniki: po trafieniu dyrektywy kazdy token jawnie (peek -> decyzja -> consume). + +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/detail/subtype.hpp b/eu07-parser/include/eu07/scene/detail/subtype.hpp new file mode 100644 index 00000000..aab42d7f --- /dev/null +++ b/eu07-parser/include/eu07/scene/detail/subtype.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include + +namespace eu07::scene::detail { + +[[nodiscard]] inline bool isNodeSubtype(const std::string_view token) noexcept { + static constexpr std::array 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 diff --git a/eu07-parser/include/eu07/scene/dispatch_table.hpp b/eu07-parser/include/eu07/scene/dispatch_table.hpp new file mode 100644 index 00000000..0df47a0c --- /dev/null +++ b/eu07-parser/include/eu07/scene/dispatch_table.hpp @@ -0,0 +1,110 @@ +#pragma once + +// Jedna tabela: płaski dispatch jak w simulationstateserializer.cpp (bez processNested). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 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 diff --git a/eu07-parser/include/eu07/scene/document.hpp b/eu07-parser/include/eu07/scene/document.hpp new file mode 100644 index 00000000..6c5f352e --- /dev/null +++ b/eu07-parser/include/eu07/scene/document.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace eu07::scene { + +struct DirectiveBlock { + std::size_t line = 0; + std::vector tokens; +}; + +struct UnknownEntry { + std::size_t line = 0; + std::string token; +}; + +struct SceneDocument { + std::vector atmo; + std::vector sky; + std::vector time; + std::vector firstInit; + std::vector trainset; + std::vector event; + std::vector include; + std::vector camera; + std::vector config; + std::vector lua; + + std::vector nodeDynamic; + std::vector nodeModel; + std::vector nodeTrackNormal; + std::vector nodeTrackSwitch; + std::vector nodeTrackRoad; + std::vector nodeTrackCross; + std::vector nodeTrackOther; + std::vector nodeTraction; + std::vector nodeTractionPower; + std::vector nodeTriangles; + std::vector nodeTriangleStrip; + std::vector nodeTriangleFan; + std::vector nodeLines; + std::vector nodeLineStrip; + std::vector nodeLineLoop; + std::vector nodeMemcell; + std::vector nodeEventlauncher; + std::vector nodeSound; + + std::vector origin; + std::vector scale; + std::vector rotate; + std::vector group; + std::vector isolated; + std::vector area; + std::vector terrain; + + std::vector description; + std::vector light; + std::vector test; + + std::vector 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 diff --git a/eu07-parser/include/eu07/scene/event.hpp b/eu07-parser/include/eu07/scene/event.hpp new file mode 100644 index 00000000..ba9ea314 --- /dev/null +++ b/eu07-parser/include/eu07/scene/event.hpp @@ -0,0 +1,37 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_event +// event name type delay … endevent + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/first_init.hpp b/eu07-parser/include/eu07/scene/first_init.hpp new file mode 100644 index 00000000..e8ce81ad --- /dev/null +++ b/eu07-parser/include/eu07/scene/first_init.hpp @@ -0,0 +1,23 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_FirstInit +// FirstInit — samo slowo kluczowe, bez end* + +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/group.hpp b/eu07-parser/include/eu07/scene/group.hpp new file mode 100644 index 00000000..d8ed9fcb --- /dev/null +++ b/eu07-parser/include/eu07/scene/group.hpp @@ -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 +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/include.hpp b/eu07-parser/include/eu07/scene/include.hpp new file mode 100644 index 00000000..0055987e --- /dev/null +++ b/eu07-parser/include/eu07/scene/include.hpp @@ -0,0 +1,73 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_include +// include file parameters end — ekspansja rekurencyjna (wariant 2) + +#include +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/include_placement.hpp b/eu07-parser/include/eu07/scene/include_placement.hpp new file mode 100644 index 00000000..81177379 --- /dev/null +++ b/eu07-parser/include/eu07/scene/include_placement.hpp @@ -0,0 +1,66 @@ +#pragma once + +// Mapowanie origin/rotate z .inc na indeksy parametrow INCL — bez heurystyki w runtime. + +#include +#include + +#include +#include +#include + +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 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 diff --git a/eu07-parser/include/eu07/scene/include_resolve.hpp b/eu07-parser/include/eu07/scene/include_resolve.hpp new file mode 100644 index 00000000..d0fc191b --- /dev/null +++ b/eu07-parser/include/eu07/scene/include_resolve.hpp @@ -0,0 +1,619 @@ +#pragma once + + + +#include + + + +#include + +#include + +#include + +#include + +#include + +#include + +#include + +#include + + + +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(std::tolower(static_cast(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(text[i]))) { + + return false; + + } + + + + while (i < text.size() && std::isdigit(static_cast(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(text[i])); ++i) { + value = value * 10 + static_cast(text[i] - '0'); + } + if (value == 0 || value > 255) { + return false; + } + + indexOut = static_cast(value); + return true; +} + +[[nodiscard]] inline std::string resolveIncludeParameter( + + const std::string_view indexText, + + const std::span 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 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& 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& 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 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& includeStack, + + const std::string& fileToken) { + + const std::filesystem::path anchor = + + baseDirectory.empty() ? std::filesystem::current_path() : baseDirectory; + + if (const std::optional 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 + + diff --git a/eu07-parser/include/eu07/scene/include_scan.hpp b/eu07-parser/include/eu07/scene/include_scan.hpp new file mode 100644 index 00000000..12589836 --- /dev/null +++ b/eu07-parser/include/eu07/scene/include_scan.hpp @@ -0,0 +1,47 @@ +#pragma once + +// Szybkie wyciagniecie sciezek z dyrektyw include — bez processScene / bake. + +#include +#include + +#include +#include +#include +#include + +namespace eu07::scene { + +[[nodiscard]] inline std::vector scanIncludeFilePaths( + const std::span tokens) { + std::vector 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 scanIncludeFilePaths( + const std::filesystem::path& sourcePath) { + const ParseResult parsed = parseFile(sourcePath); + return scanIncludeFilePaths(parsed.tokens); +} + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/include_types.hpp b/eu07-parser/include/eu07/scene/include_types.hpp new file mode 100644 index 00000000..9a44504d --- /dev/null +++ b/eu07-parser/include/eu07/scene/include_types.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace eu07::scene { + +struct ParsedInclude { + std::size_t line = 0; + std::string file; + std::vector parameters; + std::vector raw; + runtime::TransformContext siteTransform; + bool expanded = false; + std::optional error; +}; + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/isolated.hpp b/eu07-parser/include/eu07/scene/isolated.hpp new file mode 100644 index 00000000..1d85ff74 --- /dev/null +++ b/eu07-parser/include/eu07/scene/isolated.hpp @@ -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 +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/keywords.hpp b/eu07-parser/include/eu07/scene/keywords.hpp new file mode 100644 index 00000000..6b9c7fbd --- /dev/null +++ b/eu07-parser/include/eu07/scene/keywords.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include + +namespace eu07::scene { + +inline constexpr std::array 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 diff --git a/eu07-parser/include/eu07/scene/light.hpp b/eu07-parser/include/eu07/scene/light.hpp new file mode 100644 index 00000000..fe06e6cb --- /dev/null +++ b/eu07-parser/include/eu07/scene/light.hpp @@ -0,0 +1,35 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_light +// light location ambient diffuse specular endlight + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/lua.hpp b/eu07-parser/include/eu07/scene/lua.hpp new file mode 100644 index 00000000..8b63f277 --- /dev/null +++ b/eu07-parser/include/eu07/scene/lua.hpp @@ -0,0 +1,31 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_lua +// lua file + +#include +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/match.hpp b/eu07-parser/include/eu07/scene/match.hpp new file mode 100644 index 00000000..12696327 --- /dev/null +++ b/eu07-parser/include/eu07/scene/match.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +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(a[i])) != + std::tolower(static_cast(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 diff --git a/eu07-parser/include/eu07/scene/node.hpp b/eu07-parser/include/eu07/scene/node.hpp new file mode 100644 index 00000000..7b10e63b --- /dev/null +++ b/eu07-parser/include/eu07/scene/node.hpp @@ -0,0 +1,66 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node + +#include +#include +#include +#include +#include +#include + +#include +#include + +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 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 diff --git a/eu07-parser/include/eu07/scene/node/common.hpp b/eu07-parser/include/eu07/scene/node/common.hpp new file mode 100644 index 00000000..0899cc37 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/common.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +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* embedRaw); +}; + +[[nodiscard]] inline bool consumeHeader( + TokenStream& stream, + NodeHeader& header, + std::string& subtypeOut, + std::vector& 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 diff --git a/eu07-parser/include/eu07/scene/node/dynamic.hpp b/eu07-parser/include/eu07/scene/node/dynamic.hpp new file mode 100644 index 00000000..dab59af6 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/dynamic.hpp @@ -0,0 +1,128 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node::dynamic + +#include +#include +#include +#include +#include + +#include +#include +#include + +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 loadtype; + std::optional loadModifier; + std::optional destination; + bool inTrainset = false; + std::vector 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& 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(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 diff --git a/eu07-parser/include/eu07/scene/node/eventlauncher.hpp b/eu07-parser/include/eu07/scene/node/eventlauncher.hpp new file mode 100644 index 00000000..3a4642c6 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/eventlauncher.hpp @@ -0,0 +1,169 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node::eventlauncher + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace eu07::scene { + +struct EventLauncherCondition { + std::string memcell; + std::string parameter; + std::optional value1; + std::optional 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 event2; + std::optional condition; + bool trainTriggered = false; + std::vector 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& raw, + const int flag, + std::optional& out, + int& checkMask) { + std::string token; + if (!node::io::takeString(stream, raw, token)) { + return false; + } + if (token == "*") { + return true; + } + checkMask |= flag; + const std::optional value = node::io::parseDouble(token); + if (!value) { + return false; + } + out = *value; + return true; +} + +[[nodiscard]] inline bool consumeLauncherEnd(TokenStream& stream, std::vector& raw) { + return node::io::consumeEnd(stream, raw, kSubtype, kEndMarker); +} + +[[nodiscard]] inline bool parseBody( + TokenStream& stream, + std::vector& 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 diff --git a/eu07-parser/include/eu07/scene/node/header.hpp b/eu07-parser/include/eu07/scene/node/header.hpp new file mode 100644 index 00000000..3fdc19fc --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/header.hpp @@ -0,0 +1,31 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node + +#include +#include +#include +#include + +#include +#include + +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 originStack; + std::vector scaleStack; + std::size_t groupStackDepth = 0; + std::optional groupHandle; + std::optional trainsetIndex; + std::optional trainset; +}; + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/node/kinds.hpp b/eu07-parser/include/eu07/scene/node/kinds.hpp new file mode 100644 index 00000000..a6326bfd --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/kinds.hpp @@ -0,0 +1,295 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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* embedRaw) { + std::vector 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 &, const NodeHeader&, Parsed&), void (*Store)(SceneDocument&, Parsed&&)> +[[nodiscard]] inline bool parseKind( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + std::vector 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* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseModel( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseTraction( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseTractionPower( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseTriangles( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseTriangleStrip( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseTriangleFan( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseLines( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseLineStrip( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseLineLoop( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseMemcell( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseEventlauncher( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline bool parseSound( + TokenStream& stream, + SceneDocument& document, + const NodeHeader& header, + std::vector* embedRaw) { + return parseKind( + stream, document, header, embedRaw); +} + +inline constexpr std::array 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 kinds) noexcept { + for (const NodeKindDesc& kind : kinds) { + if (isKeyword(subtype, kind.subtype)) { + return &kind; + } + } + return nullptr; +} + +} // namespace eu07::scene::node::io diff --git a/eu07-parser/include/eu07/scene/node/line_loop.hpp b/eu07-parser/include/eu07/scene/node/line_loop.hpp new file mode 100644 index 00000000..7a8a2284 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/line_loop.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace eu07::scene { + +struct ParsedNodeLineLoop { + NodeHeader header; + MaterialRgb color; + double thickness = 0.0; + std::vector points; + std::vector 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/line_strip.hpp b/eu07-parser/include/eu07/scene/node/line_strip.hpp new file mode 100644 index 00000000..8c38162d --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/line_strip.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace eu07::scene { + +struct ParsedNodeLineStrip { + NodeHeader header; + MaterialRgb color; + double thickness = 0.0; + std::vector points; + std::vector 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/lines.hpp b/eu07-parser/include/eu07/scene/node/lines.hpp new file mode 100644 index 00000000..47e379b5 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/lines.hpp @@ -0,0 +1,61 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node (lines) + +#include +#include +#include +#include + +#include +#include + +namespace eu07::scene { + +struct LineSegment { + Vec3 a; + Vec3 b; +}; + +struct ParsedNodeLines { + NodeHeader header; + MaterialRgb color; + double thickness = 0.0; + std::vector segments; + std::vector 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/memcell.hpp b/eu07-parser/include/eu07/scene/node/memcell.hpp new file mode 100644 index 00000000..1e5c57bb --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/memcell.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace eu07::scene { + +struct ParsedNodeMemcell { + NodeHeader header; + Vec3 position; + std::string command; + double value1 = 0.0; + double value2 = 0.0; + std::optional trackName; + std::vector 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/mesh.hpp b/eu07-parser/include/eu07/scene/node/mesh.hpp new file mode 100644 index 00000000..848d8b30 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/mesh.hpp @@ -0,0 +1,45 @@ +#pragma once + +// Wspolne parsowanie siatki: triangle_strip, triangle_fan + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace eu07::scene::node::io { + +template +[[nodiscard]] inline bool parseTexturedMeshBody( + TokenStream& stream, + std::vector& 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 diff --git a/eu07-parser/include/eu07/scene/node/model.hpp b/eu07-parser/include/eu07/scene/node/model.hpp new file mode 100644 index 00000000..000ee83d --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/model.hpp @@ -0,0 +1,175 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node::model + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace eu07::scene { + +struct ParsedNodeModel { + NodeHeader header; + Vec3 location; + double rotationY = 0.0; + std::string modelPath; + std::string replaceableSkin; + std::optional> lightStates; + std::optional angles; + std::optional> lightColors; + std::optional inlineScale; + bool noTransition = false; + std::vector extra; + std::vector 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& raw, + MaterialRgb& color) { + SourceToken token; + if (!node::io::takeToken(stream, raw, token)) { + return false; + } + if (const std::optional 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 gv = node::io::parseDouble(g.value)) { + color.g = *gv; + } + if (const std::optional 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((packed >> 16) & 0xFF); + color.g = static_cast((packed >> 8) & 0xFF); + color.b = static_cast(packed & 0xFF); + return true; + } catch (...) { + return false; + } +} + +[[nodiscard]] inline bool parseBody( + TokenStream& stream, + std::vector& 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 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 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 diff --git a/eu07-parser/include/eu07/scene/node/parse_util.hpp b/eu07-parser/include/eu07/scene/node/parse_util.hpp new file mode 100644 index 00000000..e9d93184 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/parse_util.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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& 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 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& raw, + SourceToken& out) { + if (stream.empty()) { + return false; + } + out = stream.consume(); + appendRaw(raw, out); + return true; +} + +[[nodiscard]] inline bool takeString( + TokenStream& stream, + std::vector& 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& raw, + double& out) { + SourceToken token; + if (!takeToken(stream, raw, token)) { + return false; + } + const std::optional value = parseDouble(token.value); + if (!value) { + return false; + } + out = *value; + return true; +} + +[[nodiscard]] inline bool takeDoubleOrPlaceholder( + TokenStream& stream, + std::vector& raw, + double& out) { + SourceToken token; + if (!takeToken(stream, raw, token)) { + return false; + } + if (const std::optional 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& 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/shared.hpp b/eu07-parser/include/eu07/scene/node/shared.hpp new file mode 100644 index 00000000..7515c677 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/shared.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/node/sound.hpp b/eu07-parser/include/eu07/scene/node/sound.hpp new file mode 100644 index 00000000..4deb1ce8 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/sound.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace eu07::scene { + +struct ParsedNodeSound { + NodeHeader header; + Vec3 position; + std::string wavFile; + std::vector 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/track.hpp b/eu07-parser/include/eu07/scene/node/track.hpp new file mode 100644 index 00000000..be328a03 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/track.hpp @@ -0,0 +1,736 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node::track + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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 occupiedStop; + std::optional occupiedEnterP1; + std::optional occupiedEnterP2; + std::optional anyStop; + std::optional anyEnterP1; + std::optional anyEnterP2; +}; + +struct TrackOptionals { + std::optional velocity; + std::optional overhead; + std::optional railProfile; + std::optional trackBed; + std::optional frictionOverride; + std::optional verticalRadius; + std::optional angle1; + std::optional angle2; + std::optional fouling1; + std::optional fouling2; + std::optional sleeperModel; + TrackEvents events; + std::vector isolated; + std::vector> 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 visibility; + std::vector beziers; + TrackOptionals optionals; + std::vector 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 visibility; + std::vector beziers; + TrackOptionals optionals; + std::vector 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 visibility; + std::vector beziers; + TrackOptionals optionals; + std::vector 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 visibility; + std::vector beziers; + TrackOptionals optionals; + std::vector 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; + std::optional railVisibility; + std::vector beziers; + TrackOptionals optionals; + std::vector 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& 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& 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& raw, + CoreParams& out) { + std::vector 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(numerics[4]); + } + if (numerics.size() >= 6) { + out.damageFlag = static_cast(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& 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& 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& 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& 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& 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& raw, + std::vector& 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& 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& 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& 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& 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/traction.hpp b/eu07-parser/include/eu07/scene/node/traction.hpp new file mode 100644 index 00000000..2911956f --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/traction.hpp @@ -0,0 +1,100 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node::traction + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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 parallel; + std::vector 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& 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(damageFlag); + out.wires = static_cast(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 diff --git a/eu07-parser/include/eu07/scene/node/traction_power.hpp b/eu07-parser/include/eu07/scene/node/traction_power.hpp new file mode 100644 index 00000000..aa302305 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/traction_power.hpp @@ -0,0 +1,80 @@ +#pragma once + +// https://wiki.eu07.pl — node tractionpowersource (world/TractionPower.cpp) + +#include +#include +#include +#include +#include + +#include +#include + +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 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/triangle_fan.hpp b/eu07-parser/include/eu07/scene/node/triangle_fan.hpp new file mode 100644 index 00000000..72e11085 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/triangle_fan.hpp @@ -0,0 +1,36 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node + +#include +#include +#include + +#include +#include + +namespace eu07::scene { + +struct ParsedNodeTriangleFan { + NodeHeader header; + std::string texture; + std::vector vertices; + std::vector 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& raw, + const NodeHeader& header, + ParsedNodeTriangleFan& out) { + return node::io::parseTexturedMeshBody(stream, raw, header, kSubtype, kEndMarker, out); +} + +} // namespace node_triangle_fan + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/node/triangle_strip.hpp b/eu07-parser/include/eu07/scene/node/triangle_strip.hpp new file mode 100644 index 00000000..791796a9 --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/triangle_strip.hpp @@ -0,0 +1,36 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node + +#include +#include +#include + +#include +#include + +namespace eu07::scene { + +struct ParsedNodeTriangleStrip { + NodeHeader header; + std::string texture; + std::vector vertices; + std::vector 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& raw, + const NodeHeader& header, + ParsedNodeTriangleStrip& out) { + return node::io::parseTexturedMeshBody(stream, raw, header, kSubtype, kEndMarker, out); +} + +} // namespace node_triangle_strip + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/node/triangles.hpp b/eu07-parser/include/eu07/scene/node/triangles.hpp new file mode 100644 index 00000000..37da46ff --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/triangles.hpp @@ -0,0 +1,122 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Obiekt_node::triangles + +#include +#include +#include +#include +#include + +#include +#include + +namespace eu07::scene { + +struct ParsedNodeTriangles { + NodeHeader header; + NodeMaterial material; + std::string texture; + std::vector vertices; + std::vector 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& 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& 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& 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 diff --git a/eu07-parser/include/eu07/scene/node/types.hpp b/eu07-parser/include/eu07/scene/node/types.hpp new file mode 100644 index 00000000..a9bfe50d --- /dev/null +++ b/eu07-parser/include/eu07/scene/node/types.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +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 diff --git a/eu07-parser/include/eu07/scene/origin.hpp b/eu07-parser/include/eu07/scene/origin.hpp new file mode 100644 index 00000000..c2bf99f3 --- /dev/null +++ b/eu07-parser/include/eu07/scene/origin.hpp @@ -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 +#include +#include +#include +#include + +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 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 diff --git a/eu07-parser/include/eu07/scene/processor.hpp b/eu07-parser/include/eu07/scene/processor.hpp new file mode 100644 index 00000000..d57367b9 --- /dev/null +++ b/eu07-parser/include/eu07/scene/processor.hpp @@ -0,0 +1,243 @@ +#pragma once + + + +// https://wiki.eu07.pl/index.php?title=Plik_scenerii + + + +#include + +#include + +#include + +#include + +#include + +#include + + + +#include + +#include + + + +namespace eu07::scene { + + + +namespace detail { + + + +inline ParsedInclude* includeEntryAt(ParseContext& context, const std::size_t entryIndex) noexcept { + + if (entryIndex >= context.document.include.size()) { + + return nullptr; + + } + + return &context.document.include[entryIndex]; + +} + + + +inline void processStream(TokenStream& stream, ParseContext& context) { + + while (!stream.empty()) { + + if (dispatchDirective(stream, context)) { + + continue; + + } + + + + UnknownEntry unknown; + + unknown.line = stream.peek().sourceLine; + + unknown.token = stream.consume().value; + + context.document.unknown.push_back(std::move(unknown)); + + } + +} + + + +inline void expandInclude(ParseContext& context, const IncludeExpansionRequest& request) { + + ParsedInclude* const entry = includeEntryAt(context, request.entryIndex); + + if (entry == nullptr) { + + return; + + } + + + + if (request.file.empty()) { + + entry->error = "pusta sciezka include"; + + return; + + } + + + + const std::filesystem::path resolved = detail::resolveExpandedIncludePath( + + context.baseDirectory, context.includeStack, request.file); + + + + if (detail::isIncludeCycle(resolved, context.includeStack)) { + + entry->error = "cykl include: " + resolved.string(); + + return; + + } + + + + std::error_code existsEc; + + if (!std::filesystem::exists(resolved, existsEc)) { + + entry->error = "nie znaleziono pliku: " + resolved.string(); + + return; + + } + + + + context.includeStack.push_back(resolved.lexically_normal()); + + + + const bool wrapIncGroup = detail::isIncFile(request.file); + + if (wrapIncGroup) { + + context.scratch.openGroup(); + + } + + + + context.activeIncludeParameters.push_back(request.parameters); + + try { + + const eu07::ParseResult included = detail::parseIncludedFile(resolved, request.parameters); + + TokenStream childStream(included.tokens); + + processStream(childStream, context); + + + + if (ParsedInclude* const done = includeEntryAt(context, request.entryIndex)) { + + done->expanded = true; + + } + + } catch (const std::exception& ex) { + + if (ParsedInclude* const failed = includeEntryAt(context, request.entryIndex)) { + + failed->error = ex.what(); + + } + + } + + context.activeIncludeParameters.pop_back(); + + + + if (wrapIncGroup) { + + context.scratch.closeGroup(); + + } + + + + if (!context.includeStack.empty()) { + + context.includeStack.pop_back(); + + } + +} + + + +} // namespace detail + + + +struct SceneProcessOptions { + bool expandIncludes = true; +}; + +[[nodiscard]] inline SceneProcessResult processScene( + + const ParseResult& parsed, + + const std::filesystem::path& baseDirectory = {}, + + const SceneProcessOptions& options = {}) { + + TokenStream stream(parsed.tokens); + + SceneDocument document; + + + + std::filesystem::path includeRoot = baseDirectory; + + if (includeRoot.empty()) { + + includeRoot = std::filesystem::current_path(); + + } + + + + ParseContext context{document, {}, includeRoot, {}}; + + context.expandIncludes = options.expandIncludes; + + detail::processStream(stream, context); + + + + SceneProcessResult result; + + result.document = std::move(document); + + return result; + +} + + + +} // namespace eu07::scene + + diff --git a/eu07-parser/include/eu07/scene/report.hpp b/eu07-parser/include/eu07/scene/report.hpp new file mode 100644 index 00000000..638590ba --- /dev/null +++ b/eu07-parser/include/eu07/scene/report.hpp @@ -0,0 +1,202 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace eu07::scene { + +namespace detail { + +inline void writeHeader(std::ostream& out, const NodeHeader& header) { + out << "L" << (header.line + 1) << " range_max=" << header.rangeMax + << " range_min=" << header.rangeMin << " name=" << header.name; +} + +inline void writeVec3(std::ostream& out, const Vec3& v) { + out << v.x << ' ' << v.y << ' ' << v.z; +} + +template +inline void writeNodeList(std::ostream& out, const std::string_view section, const std::vector& nodes) { + out << "\n[" << section << "] count=" << nodes.size() << '\n'; +} + +} // namespace detail + +inline void writeSceneReport(const std::filesystem::path& outPath, const SceneDocument& doc) { + std::ofstream out(outPath); + if (!out) { + throw std::runtime_error("Nie mozna zapisac: " + outPath.string()); + } + + out << "# raport scenariusza — sparsowane struktury node + tokeny pozostalych dyrektyw\n\n"; + out << "[podsumowanie] node=" << countNodeInstances(doc) << " razem=" << countDirectiveInstances(doc) + << " nieznane=" << doc.unknown.size() << " include=" << doc.include.size() << '\n'; + + detail::writeNodeList(out, "include", doc.include); + for (const ParsedInclude& inc : doc.include) { + out << "L" << (inc.line + 1) << " file=" << inc.file; + if (!inc.parameters.empty()) { + out << " params=" << inc.parameters.size(); + } + out << " expanded=" << (inc.expanded ? "yes" : "no"); + if (inc.error) { + out << " error=" << *inc.error; + } + out << '\n'; + } + + detail::writeNodeList(out, "node_dynamic", doc.nodeDynamic); + for (const ParsedNodeDynamic& node : doc.nodeDynamic) { + detail::writeHeader(out, node.header); + out << " folder=" << node.datafolder << " skin=" << node.skinfile << " mmd=" << node.mmdfile + << " track=" << node.pathname << " driver=" << node.driverType + << " coupling=" << node.coupling.raw; + if (node.inTrainset) { + out << " trainset"; + } + if (node.destination) { + out << " dest=" << *node.destination; + } + out << '\n'; + } + + detail::writeNodeList(out, "node_model", doc.nodeModel); + for (const ParsedNodeModel& node : doc.nodeModel) { + detail::writeHeader(out, node.header); + out << " pos="; + detail::writeVec3(out, node.location); + out << " rotY=" << node.rotationY << " model=" << node.modelPath << '\n'; + } + + detail::writeNodeList(out, "node_track_normal", doc.nodeTrackNormal); + for (const ParsedTrackNormal& node : doc.nodeTrackNormal) { + detail::writeHeader(out, node.header); + out << " kind=" << node.trajectoryKind << " gauge=" << node.gauge << " env=" << node.environment + << " beziers=" << node.beziers.size() << '\n'; + } + + detail::writeNodeList(out, "node_track_switch", doc.nodeTrackSwitch); + for (const ParsedTrackSwitch& node : doc.nodeTrackSwitch) { + detail::writeHeader(out, node.header); + out << " gauge=" << node.gauge << " env=" << node.environment + << " beziers=" << node.beziers.size() << '\n'; + } + + detail::writeNodeList(out, "node_track_road", doc.nodeTrackRoad); + for (const ParsedTrackRoad& node : doc.nodeTrackRoad) { + detail::writeHeader(out, node.header); + out << " width=" << node.roadWidth << " env=" << node.environment + << " beziers=" << node.beziers.size() << '\n'; + } + + detail::writeNodeList(out, "node_track_cross", doc.nodeTrackCross); + for (const ParsedTrackCross& node : doc.nodeTrackCross) { + detail::writeHeader(out, node.header); + out << " width=" << node.roadWidth << " env=" << node.environment + << " beziers=" << node.beziers.size() << '\n'; + } + + detail::writeNodeList(out, "node_track_other", doc.nodeTrackOther); + for (const ParsedTrackOther& node : doc.nodeTrackOther) { + detail::writeHeader(out, node.header); + out << " kind=" << node.trajectoryKind << " width=" << node.width << " env=" << node.environment + << " beziers=" << node.beziers.size() << '\n'; + } + + detail::writeNodeList(out, "node_traction", doc.nodeTraction); + for (const ParsedNodeTraction& node : doc.nodeTraction) { + detail::writeHeader(out, node.header); + out << " supply=" << node.powerSourceName << " U=" << node.nominalVoltage << '\n'; + } + + detail::writeNodeList(out, "node_traction_power", doc.nodeTractionPower); + for (const ParsedNodeTractionPowerSource& node : doc.nodeTractionPower) { + detail::writeHeader(out, node.header); + out << " U=" << node.nominalVoltage << " Imax=" << node.maxOutputCurrent; + if (node.recuperation) { + out << " recuperation"; + } + if (node.section) { + out << " section"; + } + out << '\n'; + } + + detail::writeNodeList(out, "node_triangles", doc.nodeTriangles); + for (const ParsedNodeTriangles& node : doc.nodeTriangles) { + detail::writeHeader(out, node.header); + out << " tex=" << node.texture << " vertices=" << node.vertices.size() << '\n'; + } + + detail::writeNodeList(out, "node_triangle_strip", doc.nodeTriangleStrip); + for (const ParsedNodeTriangleStrip& node : doc.nodeTriangleStrip) { + detail::writeHeader(out, node.header); + out << " tex=" << node.texture << " vertices=" << node.vertices.size() << '\n'; + } + + detail::writeNodeList(out, "node_triangle_fan", doc.nodeTriangleFan); + for (const ParsedNodeTriangleFan& node : doc.nodeTriangleFan) { + detail::writeHeader(out, node.header); + out << " tex=" << node.texture << " vertices=" << node.vertices.size() << '\n'; + } + + detail::writeNodeList(out, "node_lines", doc.nodeLines); + for (const ParsedNodeLines& node : doc.nodeLines) { + detail::writeHeader(out, node.header); + out << " segments=" << node.segments.size() << '\n'; + } + + detail::writeNodeList(out, "node_line_strip", doc.nodeLineStrip); + for (const ParsedNodeLineStrip& node : doc.nodeLineStrip) { + detail::writeHeader(out, node.header); + out << " points=" << node.points.size() << '\n'; + } + + detail::writeNodeList(out, "node_line_loop", doc.nodeLineLoop); + for (const ParsedNodeLineLoop& node : doc.nodeLineLoop) { + detail::writeHeader(out, node.header); + out << " points=" << node.points.size() << '\n'; + } + + detail::writeNodeList(out, "node_memcell", doc.nodeMemcell); + for (const ParsedNodeMemcell& node : doc.nodeMemcell) { + detail::writeHeader(out, node.header); + out << " cmd=" << node.command; + if (node.trackName) { + out << " track=" << *node.trackName; + } + out << '\n'; + } + + detail::writeNodeList(out, "node_eventlauncher", doc.nodeEventlauncher); + for (const ParsedNodeEventlauncher& node : doc.nodeEventlauncher) { + detail::writeHeader(out, node.header); + out << " key=" << node.key << " event1=" << node.event1; + if (node.event2) { + out << " event2=" << *node.event2; + } + if (node.condition) { + out << " condition"; + } + if (node.trainTriggered) { + out << " traintriggered"; + } + out << '\n'; + } + + detail::writeNodeList(out, "node_sound", doc.nodeSound); + for (const ParsedNodeSound& node : doc.nodeSound) { + detail::writeHeader(out, node.header); + out << " wav=" << node.wavFile << '\n'; + } +} + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/rotate.hpp b/eu07-parser/include/eu07/scene/rotate.hpp new file mode 100644 index 00000000..53f7af7f --- /dev/null +++ b/eu07-parser/include/eu07/scene/rotate.hpp @@ -0,0 +1,36 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_rotate +// rotate x y z — nadpisuje rotację, bez endrotate i bez połykania kolejnych tokenów. + +#include +#include +#include +#include +#include + +namespace eu07::scene::rotate { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "rotate")) { + return false; + } + + DirectiveBlock block; + block.line = stream.consume().sourceLine; + + Vec3 rotation; + std::vector raw; + if (!node::io::takeDoubleOrPlaceholder(stream, raw, rotation.x) || + !node::io::takeDoubleOrPlaceholder(stream, raw, rotation.y) || + !node::io::takeDoubleOrPlaceholder(stream, raw, rotation.z)) { + return false; + } + + block.tokens = std::move(raw); + ctx.scratch.setRotation(rotation); + ctx.document.rotate.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::rotate diff --git a/eu07-parser/include/eu07/scene/runtime.hpp b/eu07-parser/include/eu07/scene/runtime.hpp new file mode 100644 index 00000000..62a25e9a --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime.hpp @@ -0,0 +1,23 @@ +#pragma once + +// Kontrakt runtime MaSzyny — structy docelowe po state_serializer. +// +// Zrodla w maszyna-fresh: +// simulation/simulationstateserializer.cpp (deserialize_*, transform) +// scene/scenenode.h / scenenode.cpp (shape_node, lines_node, basic_node) +// model/AnimModel.cpp (TAnimModel::Load) +// world/Track.cpp, Segment.cpp (TTrack::Load, segment_data) +// world/Traction.cpp, TractionPower.cpp +// world/MemCell.cpp, EvLaunch.cpp +// world/Event.cpp (make_event, basic_event) +// +// Warstwa Parsed* (document.hpp) = AST z tekstu. +// Warstwa runtime::* = pola, ktore symulator trzyma po imporcie. + +#include +#include +#include +#include +#include +#include +#include diff --git a/eu07-parser/include/eu07/scene/runtime/basic_node.hpp b/eu07-parser/include/eu07/scene/runtime/basic_node.hpp new file mode 100644 index 00000000..043df0f2 --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/basic_node.hpp @@ -0,0 +1,45 @@ +#pragma once + +// scenenode.h — basic_node (wspolna baza TTrack, TAnimModel, TMemCell, …) + +#include + +#include +#include +#include + +namespace eu07::scene::runtime { + +// Snapshot stosu origin/scale/rotate z momentu parsowania node'a (do detokenizacji). +struct TransformContext { + std::vector originStack; + std::vector scaleStack; + Vec3 rotation{}; + std::size_t groupStackDepth = 0; +}; + +struct BasicNode { + BoundingArea area; + double rangeSquaredMin = 0.0; + double rangeSquaredMax = 0.0; + bool visible = true; + std::string name; + std::string nodeType; + std::size_t groupHandle = 0; + bool groupValid = false; + TransformContext transform; + std::string uuid; + bool dirty = false; +}; + +[[nodiscard]] inline BasicNode makeBasicNode(const NodeData& data) { + BasicNode node; + node.name = (data.name == "none") ? std::string{} : data.name; + node.nodeType = data.type; + node.rangeSquaredMin = data.rangeMin * data.rangeMin; + node.rangeSquaredMax = + data.rangeMax >= 0.0 ? data.rangeMax * data.rangeMax : std::numeric_limits::max(); + return node; +} + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/runtime/directives.hpp b/eu07-parser/include/eu07/scene/runtime/directives.hpp new file mode 100644 index 00000000..25f5dc1e --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/directives.hpp @@ -0,0 +1,77 @@ +#pragma once + +// Dyrektywy scenariusza poza `node`: trainset, event, transform stack. + +#include + +#include +#include +#include +#include + +namespace eu07::scene::runtime { + +// trainset / endtrainset +struct RuntimeTrainset { + std::string name; + std::string track; + float offset = 0.f; + float velocity = 0.f; + std::unordered_map assignment; + std::vector vehicleIndices; + std::vector couplings; + std::size_t driverIndex = static_cast(-1); +}; + +// event / endevent — world/Event.h basic_event + podklasy +enum class EventType : std::uint8_t { + AddValues, + UpdateValues, + CopyValues, + GetValues, + PutValues, + Whois, + LogValues, + Multiple, + Switch, + TrackVel, + Sound, + Texture, + Animation, + Lights, + Voltage, + Visible, + Friction, + Message, + Unknown, +}; + +struct RuntimeEvent { + std::string name; + EventType type = EventType::Unknown; + double delay = 0.0; + std::vector targets; + double delayRandom = 0.0; + double delayDeparture = 0.0; + bool ignored = false; + bool passive = false; + // type-specific payload — union lub osobne structy w przyszlosci + std::vector> payload; +}; + +enum class TransformOp : std::uint8_t { + PushOrigin, + PopOrigin, + PushScale, + PopScale, + SetRotation, + GroupBegin, + GroupEnd, +}; + +struct TransformDirective { + TransformOp op = TransformOp::PushOrigin; + Vec3 vector{}; +}; + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/runtime/nodes.hpp b/eu07-parser/include/eu07/scene/runtime/nodes.hpp new file mode 100644 index 00000000..c3956908 --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/nodes.hpp @@ -0,0 +1,220 @@ +#pragma once + +// Payload per typ `node` — odpowiedniki klas w maszyna-fresh. + +#include +#include + +#include +#include +#include +#include + +namespace eu07::scene::runtime { + +// --- model → TAnimModel (model/AnimModel.h) --- + +struct RuntimeModelInstance { + BasicNode node; + Vec3 location; + Vec3 angles; + Vec3 scale{1.0, 1.0, 1.0}; + std::string modelFile; + std::string textureFile; + std::vector lightStates; + std::vector lightColors; + Vec3 inlineScale{1.0, 1.0, 1.0}; + bool transition = true; + bool isTerrain = false; +}; + +// --- triangles / triangle_strip / triangle_fan → shape_node --- + +struct RuntimeShapeNode { + BasicNode node; + bool translucent = false; + std::string materialPath; + LightingData lighting; + Vec3 origin; + std::vector vertices; +}; + +// --- lines / line_strip / line_loop → lines_node --- + +struct RuntimeLinesNode { + BasicNode node; + LightingData lighting; + float lineWidth = 1.f; + Vec3 origin; + std::vector vertices; +}; + +// --- track → TTrack (world/Track.h) --- + +enum class TrackType : std::uint8_t { + Unknown, + Normal, + Switch, + Table, + Cross, + Tributary, +}; + +enum class TrackCategory : std::uint8_t { + Rail = 1, + Road = 2, + Water = 4, +}; + +enum class TrackEnvironment : std::int8_t { + Unknown = -1, + Flat = 0, + Mountains, + Canyon, + Tunnel, + Bridge, + Bank, +}; + +struct TrackVisibility { + std::string material1; + float texLength = 4.f; + std::string material2; + float texHeight1 = 0.f; + float texWidth = 0.f; + float texSlope = 0.f; +}; + +struct RuntimeTrack { + BasicNode node; + TrackType trackType = TrackType::Unknown; + TrackCategory category = TrackCategory::Rail; + float length = 0.f; + float trackWidth = 0.f; + float friction = 0.f; + float soundDistance = 0.f; + int qualityFlag = 0; + int damageFlag = 0; + TrackEnvironment environment = TrackEnvironment::Unknown; + std::optional visibility; + std::vector paths; + // tail TTrack::Load (event0, fouling, velocity, …) — rozszerzalne pozniej + std::vector> tailKeywords; +}; + +// --- traction → TTraction (world/Traction.h) --- + +enum class TractionWireMaterial : std::uint8_t { + None = 0, + Copper = 1, + Aluminium = 2, +}; + +struct RuntimeTraction { + BasicNode node; + std::string powerSupplyName; + float nominalVoltage = 0.f; + float maxCurrent = 0.f; + float resistivityOhmPerM = 0.f; + double resistivityLegacy = 0.0; + std::string materialRaw = "cu"; + TractionWireMaterial material = TractionWireMaterial::Copper; + float wireThickness = 0.f; + int damageFlag = 0; + Vec3 wireP1; + Vec3 wireP2; + Vec3 wireP3; + Vec3 wireP4; + double minHeight = 0.0; + double segmentLength = 0.0; + int wireCount = 0; + float wireOffset = 0.f; + std::optional parallelName; +}; + +// --- tractionpowersource → TTractionPowerSource (world/TractionPower.cpp) --- + +enum class PowerSourceModifier : std::uint8_t { + None, + Recuperation, + Section, +}; + +struct RuntimeTractionPowerSource { + BasicNode node; + Vec3 position{}; + float nominalVoltage = 0.f; + float voltageFrequency = 0.f; + double internalResistanceLegacy = 0.2; + float internalResistance = 0.2f; + float maxOutputCurrent = 0.f; + float fastFuseTimeout = 0.f; + float fastFuseRepetition = 0.f; + float slowFuseTimeout = 0.f; + PowerSourceModifier modifier = PowerSourceModifier::None; +}; + +// --- memcell → TMemCell (world/MemCell.h) --- + +struct RuntimeMemCell { + BasicNode node; + std::string text; + double value1 = 0.0; + double value2 = 0.0; + std::optional trackName; +}; + +// --- eventlauncher → TEventLauncher (world/EvLaunch.h) --- + +struct EventLauncherCondition { + std::string memcellName; + std::string compareText; + double compareValue1 = 0.0; + double compareValue2 = 0.0; + int checkMask = 0; +}; + +struct RuntimeEventLauncher { + BasicNode node; + Vec3 location; + double radiusSquared = 0.0; + std::string activationKeyRaw; + int activationKey = 0; + double deltaTime = -1.0; + std::string event1Name; + std::string event2Name; + std::optional condition; + bool trainTriggered = false; + int launchHour = -1; + int launchMinute = -1; +}; + +// --- dynamic → TDynamicObject (vehicle/DynObj) --- + +struct RuntimeDynamicObject { + BasicNode node; + std::string dataFolder; + std::string skinFile; + std::string mmdFile; + std::string trackName; + double offset = -1.0; + std::string driverType; + int coupling = 3; + std::string couplingRaw = "3"; + std::string couplingParams; + float velocity = 0.f; + int loadCount = 0; + std::string loadType; + std::optional destination; + std::optional trainsetIndex; +}; + +// --- sound → sound_source (audio/sound.h) --- + +struct RuntimeSoundSource { + BasicNode node; + Vec3 location; + std::string wavFile; +}; + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/runtime/scene.hpp b/eu07-parser/include/eu07/scene/runtime/scene.hpp new file mode 100644 index 00000000..25f40663 --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/scene.hpp @@ -0,0 +1,53 @@ +#pragma once + +// Zbiorczy dokument runtime po bake. + +#include +#include + +#include +#include + +namespace eu07::scene::runtime { + +// Stare szkice tagow — nieuzywane (runtime uzywa chunkow czteroliterowych w runtime_module.hpp). +enum class SectionTag : std::uint32_t { + FileHeader = 0x45443742, // 'ED7B' placeholder + Transform = 1, + Trainset = 2, + Event = 3, + NodeModel = 10, + NodeShape = 11, + NodeLines = 12, + NodeTrack = 13, + NodeTraction = 14, + NodePowerSource = 15, + NodeMemCell = 16, + NodeEventLauncher = 17, + NodeDynamic = 18, + NodeSound = 19, +}; + +struct RuntimeScene { + RuntimeScratchpad scratch; + + std::vector transforms; + std::vector trainsets; + std::vector events; + + std::vector models; + std::vector shapes; + std::vector lines; + std::vector tracks; + std::vector traction; + std::vector powerSources; + std::vector memcells; + std::vector eventLaunchers; + std::vector dynamics; + std::vector sounds; + + // FirstInit — marker inicjalizacji scenariusza (bez payloadu) + std::uint32_t firstInitCount = 0; +}; + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/runtime/scratch.hpp b/eu07-parser/include/eu07/scene/runtime/scratch.hpp new file mode 100644 index 00000000..d64585b1 --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/scratch.hpp @@ -0,0 +1,38 @@ +#pragma once + +// scene/scene.h — scratch_data (kontekst parsowania, nie persystowany 1:1) + +#include + +#include +#include +#include +#include + +namespace eu07::scene::runtime { + +struct RuntimeScratchpad { + struct LocationState { + std::vector offsetStack; + std::vector scaleStack; + Vec3 rotation{}; + } location; + + struct TrainsetState { + bool isOpen = false; + std::string name; + std::string track; + float offset = 0.f; + float velocity = 0.f; + std::vector vehicleIndices; + std::vector couplings; + std::size_t driverIndex = static_cast(-1); + std::unordered_map assignment; + } trainset; + + std::string scenarioName; + bool initialized = false; + bool timeInitialized = false; +}; + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/runtime/transform.hpp b/eu07-parser/include/eu07/scene/runtime/transform.hpp new file mode 100644 index 00000000..4a654d3d --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/transform.hpp @@ -0,0 +1,213 @@ +#pragma once + +// simulation/simulationstateserializer.cpp — transform() + basic_region::insert + +#include +#include +#include +#include + +#include +#include + +namespace eu07::scene::runtime { + +// Punkt modelu / memcell / sound / eventlauncher / tractionpowersource: +// rotateY → scale (iloczyn osiowy) → offset. +[[nodiscard]] inline Vec3 transformPoint(Vec3 location, const RuntimeScratchpad& scratch) { + if (scratch.location.rotation.y != 0.0) { + const double rad = scratch.location.rotation.y * (3.14159265358979323846 / 180.0); + const double c = std::cos(rad); + const double s = std::sin(rad); + const double x = location.x * c + location.z * s; + const double z = -location.x * s + location.z * c; + location.x = x; + location.z = z; + } + + if (!scratch.location.scaleStack.empty()) { + const Vec3& sc = scratch.location.scaleStack.back(); + location.x *= sc.x; + location.y *= sc.y; + location.z *= sc.z; + } + + if (!scratch.location.offsetStack.empty()) { + const Vec3& off = scratch.location.offsetStack.back(); + location.x += off.x; + location.y += off.y; + location.z += off.z; + } + + return location; +} + +// Shape/lines: rotacja Z → X → Y na wierzcholkach, potem offset. Bez scale na geometrii. +inline void transformShapeVertices(std::vector& vertices, const RuntimeScratchpad& scratch) { + if (scratch.location.rotation.x != 0.0 || scratch.location.rotation.y != 0.0 || + scratch.location.rotation.z != 0.0) { + const double rx = scratch.location.rotation.x * (3.14159265358979323846 / 180.0); + const double ry = scratch.location.rotation.y * (3.14159265358979323846 / 180.0); + const double rz = scratch.location.rotation.z * (3.14159265358979323846 / 180.0); + + auto rotateZ = [&](Vec3& v) { + const double c = std::cos(rz); + const double s = std::sin(rz); + const double x = v.x * c - v.y * s; + const double y = v.x * s + v.y * c; + v.x = x; + v.y = y; + }; + auto rotateX = [&](Vec3& v) { + const double c = std::cos(rx); + const double s = std::sin(rx); + const double y = v.y * c - v.z * s; + const double z = v.y * s + v.z * c; + v.y = y; + v.z = z; + }; + auto rotateY = [&](Vec3& v) { + const double c = std::cos(ry); + const double s = std::sin(ry); + const double x = v.x * c + v.z * s; + const double z = -v.x * s + v.z * c; + v.x = x; + v.z = z; + }; + + for (WorldVertex& vtx : vertices) { + rotateZ(vtx.position); + rotateX(vtx.position); + rotateY(vtx.position); + Vec3 n{vtx.normal.x, vtx.normal.y, vtx.normal.z}; + rotateZ(n); + rotateX(n); + rotateY(n); + vtx.normal = n; + } + } + + if (!scratch.location.offsetStack.empty()) { + const Vec3& off = scratch.location.offsetStack.back(); + for (WorldVertex& vtx : vertices) { + vtx.position.x += off.x; + vtx.position.y += off.y; + vtx.position.z += off.z; + } + } +} + +[[nodiscard]] inline RuntimeScratchpad scratchFromTransform(const TransformContext& transform) { + RuntimeScratchpad scratch; + scratch.location.offsetStack = transform.originStack; + scratch.location.scaleStack = transform.scaleStack; + scratch.location.rotation = transform.rotation; + return scratch; +} + +[[nodiscard]] inline Vec3 inverseTransformPoint(Vec3 location, const TransformContext& transform) { + const RuntimeScratchpad scratch = scratchFromTransform(transform); + + if (!scratch.location.offsetStack.empty()) { + const Vec3& off = scratch.location.offsetStack.back(); + location.x -= off.x; + location.y -= off.y; + location.z -= off.z; + } + + if (!scratch.location.scaleStack.empty()) { + const Vec3& sc = scratch.location.scaleStack.back(); + if (sc.x != 0.0) { + location.x /= sc.x; + } + if (sc.y != 0.0) { + location.y /= sc.y; + } + if (sc.z != 0.0) { + location.z /= sc.z; + } + } + + if (scratch.location.rotation.y != 0.0) { + const double rad = -scratch.location.rotation.y * (3.14159265358979323846 / 180.0); + const double c = std::cos(rad); + const double s = std::sin(rad); + const double x = location.x * c + location.z * s; + const double z = -location.x * s + location.z * c; + location.x = x; + location.z = z; + } + + return location; +} + +inline void inverseTransformShapeVertices( + std::vector& vertices, + const TransformContext& transform) { + const RuntimeScratchpad scratch = scratchFromTransform(transform); + + if (!scratch.location.offsetStack.empty()) { + const Vec3& off = scratch.location.offsetStack.back(); + for (WorldVertex& vtx : vertices) { + vtx.position.x -= off.x; + vtx.position.y -= off.y; + vtx.position.z -= off.z; + } + } + + if (scratch.location.rotation.x != 0.0 || scratch.location.rotation.y != 0.0 || + scratch.location.rotation.z != 0.0) { + const double rx = -scratch.location.rotation.x * (3.14159265358979323846 / 180.0); + const double ry = -scratch.location.rotation.y * (3.14159265358979323846 / 180.0); + const double rz = -scratch.location.rotation.z * (3.14159265358979323846 / 180.0); + + auto rotateY = [&](Vec3& v) { + const double c = std::cos(ry); + const double s = std::sin(ry); + const double x = v.x * c + v.z * s; + const double z = -v.x * s + v.z * c; + v.x = x; + v.z = z; + }; + auto rotateX = [&](Vec3& v) { + const double c = std::cos(rx); + const double s = std::sin(rx); + const double y = v.y * c - v.z * s; + const double z = v.y * s + v.z * c; + v.y = y; + v.z = z; + }; + auto rotateZ = [&](Vec3& v) { + const double c = std::cos(rz); + const double s = std::sin(rz); + const double x = v.x * c - v.y * s; + const double y = v.x * s + v.y * c; + v.x = x; + v.y = y; + }; + + for (WorldVertex& vtx : vertices) { + rotateY(vtx.position); + rotateX(vtx.position); + rotateZ(vtx.position); + Vec3 n{vtx.normal.x, vtx.normal.y, vtx.normal.z}; + rotateY(n); + rotateX(n); + rotateZ(n); + vtx.normal = n; + } + } +} + +[[nodiscard]] inline Vec3 subtractOriginOffset(const Vec3& world, const TransformContext& transform) { + Vec3 local = world; + if (!transform.originStack.empty()) { + const Vec3& off = transform.originStack.back(); + local.x -= off.x; + local.y -= off.y; + local.z -= off.z; + } + return local; +} + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/runtime/types.hpp b/eu07-parser/include/eu07/scene/runtime/types.hpp new file mode 100644 index 00000000..85e05845 --- /dev/null +++ b/eu07-parser/include/eu07/scene/runtime/types.hpp @@ -0,0 +1,65 @@ +#pragma once + +// Kontrakt danych po deserializacji w MaSzynie (maszyna-fresh). +// Odpowiedniki: scene/scenenode.h, model/vertex.h, scene/scene.h (scratch_data). +// Parsed* w tym repo = AST z tekstu; Runtime* = docelowe pola symulatora. + +#include + +#include +#include +#include +#include + +namespace eu07::scene::runtime { + +using Vec3 = scene::Vec3; + +struct Vec4 { + float x = 0.f; + float y = 0.f; + float z = 0.f; + float w = 1.f; +}; + +// model/vertex.h — world_vertex +struct WorldVertex { + Vec3 position; + Vec3 normal; + double u = 0.0; + double v = 0.0; +}; + +// scenenode.h — lighting_data (w pliku 0–255, w runtime float 0–1) +struct LightingData { + Vec4 diffuse{0.8f, 0.8f, 0.8f, 1.f}; + Vec4 ambient{0.2f, 0.2f, 0.2f, 1.f}; + Vec4 specular{0.f, 0.f, 0.f, 1.f}; +}; + +// scenenode.h — bounding_area +struct BoundingArea { + Vec3 center; + float radius = -1.f; +}; + +// scenenode.h — node_data (naglowek kazdego `node`) +struct NodeData { + double rangeMin = 0.0; + double rangeMax = std::numeric_limits::max(); + std::string name; + std::string type; +}; + +// Segment.h — segment_data (sciezka Bezier toru/trakcji) +struct SegmentPath { + Vec3 pStart; + double rollStart = 0.0; + Vec3 cpOut; + Vec3 cpIn; + Vec3 pEnd; + double rollEnd = 0.0; + double radius = 0.0; +}; + +} // namespace eu07::scene::runtime diff --git a/eu07-parser/include/eu07/scene/scale.hpp b/eu07-parser/include/eu07/scene/scale.hpp new file mode 100644 index 00000000..1390a58a --- /dev/null +++ b/eu07-parser/include/eu07/scene/scale.hpp @@ -0,0 +1,36 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_scale +// scale x y z … endscale — push skali na stos. + +#include +#include +#include +#include +#include + +namespace eu07::scene::scale { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "scale")) { + return false; + } + + DirectiveBlock block; + block.line = stream.consume().sourceLine; + + Vec3 factor; + std::vector raw; + if (!node::io::takeDouble(stream, raw, factor.x) || + !node::io::takeDouble(stream, raw, factor.y) || + !node::io::takeDouble(stream, raw, factor.z)) { + return false; + } + + block.tokens = std::move(raw); + ctx.scratch.pushScale(factor); + ctx.document.scale.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::scale diff --git a/eu07-parser/include/eu07/scene/scratch.hpp b/eu07-parser/include/eu07/scene/scratch.hpp new file mode 100644 index 00000000..9b757b01 --- /dev/null +++ b/eu07-parser/include/eu07/scene/scratch.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace eu07::scene { + +struct SceneScratchpad { + struct LocationState { + std::vector originStack; + std::vector scaleStack; + Vec3 rotation{}; + } location; + + struct GroupState { + std::vector activeStack; + std::size_t nextHandle = 1; + } group; + + struct TrainsetState { + bool isOpen = false; + std::optional docIndex; + std::string name; + std::string track; + double offset = 0.0; + double velocity = 0.0; + std::vector> assignment; + } trainset; + + [[nodiscard]] Vec3 currentOrigin() const noexcept { + if (location.originStack.empty()) { + return {}; + } + return location.originStack.back(); + } + + void pushOrigin(const Vec3 offset) { + const Vec3 parent = currentOrigin(); + location.originStack.push_back( + {parent.x + offset.x, parent.y + offset.y, parent.z + offset.z}); + } + + void popOrigin() noexcept { + if (!location.originStack.empty()) { + location.originStack.pop_back(); + } + } + + [[nodiscard]] Vec3 currentScale() const noexcept { + if (location.scaleStack.empty()) { + return {1.0, 1.0, 1.0}; + } + return location.scaleStack.back(); + } + + void pushScale(const Vec3 factor) { + const Vec3 parent = currentScale(); + location.scaleStack.push_back( + {parent.x * factor.x, parent.y * factor.y, parent.z * factor.z}); + } + + void popScale() noexcept { + if (!location.scaleStack.empty()) { + location.scaleStack.pop_back(); + } + } + + void setRotation(const Vec3 rotation) noexcept { + location.rotation = rotation; + } + + void openGroup() { + if (group.activeStack.empty()) { + group.activeStack.push_back(group.nextHandle++); + return; + } + group.activeStack.push_back(group.activeStack.back()); + } + + void closeGroup() noexcept { + if (!group.activeStack.empty()) { + group.activeStack.pop_back(); + } + } + + [[nodiscard]] std::optional activeGroupHandle() const noexcept { + if (group.activeStack.empty()) { + return std::nullopt; + } + return group.activeStack.back(); + } + + void openTrainset() { + trainset = TrainsetState{}; + trainset.isOpen = true; + } + + void closeTrainset() noexcept { + trainset.isOpen = false; + trainset.docIndex.reset(); + } +}; + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/sky.hpp b/eu07-parser/include/eu07/scene/sky.hpp new file mode 100644 index 00000000..ab7eab46 --- /dev/null +++ b/eu07-parser/include/eu07/scene/sky.hpp @@ -0,0 +1,34 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_sky +// sky sky endsky + +#include +#include +#include +#include + +namespace eu07::scene::sky { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "sky")) { + 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(); + } + if (session.at("endsky")) { + session.take(); + } + + ctx.document.sky.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::sky diff --git a/eu07-parser/include/eu07/scene/terrain.hpp b/eu07-parser/include/eu07/scene/terrain.hpp new file mode 100644 index 00000000..5961e30b --- /dev/null +++ b/eu07-parser/include/eu07/scene/terrain.hpp @@ -0,0 +1,34 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_terrain +// terrain terrain endterrain + +#include +#include +#include +#include + +namespace eu07::scene::terrain { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "terrain")) { + 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(); + } + if (session.at("endterrain")) { + session.take(); + } + + ctx.document.terrain.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::terrain diff --git a/eu07-parser/include/eu07/scene/test.hpp b/eu07-parser/include/eu07/scene/test.hpp new file mode 100644 index 00000000..de7a09e3 --- /dev/null +++ b/eu07-parser/include/eu07/scene/test.hpp @@ -0,0 +1,32 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_test +// legacy — skip_until(endtest), bez parsowania zawartości. + +#include +#include +#include + +namespace eu07::scene::test { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "test")) { + return false; + } + + DirectiveBlock block; + block.line = stream.consume().sourceLine; + + while (!stream.empty() && !isKeyword(stream.peek().value, "endtest")) { + block.tokens.push_back(stream.consume()); + } + + if (!stream.empty() && isKeyword(stream.peek().value, "endtest")) { + block.tokens.push_back(stream.consume()); + } + + ctx.document.test.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::test diff --git a/eu07-parser/include/eu07/scene/time.hpp b/eu07-parser/include/eu07/scene/time.hpp new file mode 100644 index 00000000..4ecd6fca --- /dev/null +++ b/eu07-parser/include/eu07/scene/time.hpp @@ -0,0 +1,35 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_time +// time startTime sunsetTime sunriseTime endtime + +#include +#include +#include +#include + +namespace eu07::scene::time { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "time")) { + 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("endtime")) { + session.take(); + break; + } + session.take(); + } + + ctx.document.time.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::time diff --git a/eu07-parser/include/eu07/scene/track_routes.hpp b/eu07-parser/include/eu07/scene/track_routes.hpp new file mode 100644 index 00000000..e6e88f7e --- /dev/null +++ b/eu07-parser/include/eu07/scene/track_routes.hpp @@ -0,0 +1,608 @@ +#pragma once + +// Trasy z odcinków track: łączenie wyłącznie po geometrii końców, trasy jak najdłuższe. + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace eu07::scene { + +enum class TrackSegmentKind { Normal, Switch, Road, Cross, Other }; + +struct TrackSegmentRef { + TrackSegmentKind kind = TrackSegmentKind::Normal; + std::size_t index = 0; +}; + +struct TrackSegmentEndpoints { + TrackSegmentRef ref; + std::string name; + std::size_t sourceLine = 0; + Vec3 start{}; + Vec3 end{}; + double length = 0.0; + bool hasGeometry = false; +}; + +struct TrackConnection { + std::size_t segmentA = 0; + std::size_t segmentB = 0; + bool aAtStart = false; + bool bAtStart = false; + double distance = 0.0; +}; + +struct TrackRouteLink { + std::size_t segmentIndex = 0; + bool reversed = false; +}; + +struct TrackRoute { + std::string label; + std::vector links; + bool closed = false; + double lengthSum = 0.0; +}; + +struct TrackRouteBuildOptions { + double endpointTolerance = 0.15; + double gridCellSize = 25.0; + bool includeRoads = false; + bool includeCross = false; + bool includeOther = false; +}; + +struct TrackRouteBuildResult { + std::vector segments; + std::vector connections; + std::vector routes; + std::size_t skippedWithoutGeometry = 0; +}; + +namespace detail::track_routes { + +inline constexpr std::size_t kInvalidSlot = static_cast(-1); + +inline Vec3 addOffset(const Vec3 point, const Vec3 offset) noexcept { + return {point.x + offset.x, point.y + offset.y, point.z + offset.z}; +} + +inline double distance(const Vec3& a, const Vec3& b) noexcept { + const double dx = a.x - b.x; + const double dy = a.y - b.y; + const double dz = a.z - b.z; + return std::sqrt(dx * dx + dy * dy + dz * dz); +} + +inline std::optional> segmentWorldEnds( + const std::vector& beziers, + const Vec3& originOffset) { + if (beziers.empty()) { + return std::nullopt; + } + return std::pair{ + addOffset(beziers.front().p1, originOffset), + addOffset(beziers.back().p2, originOffset), + }; +} + +template +inline void appendSegment( + std::vector& out, + const TrackSegmentKind kind, + const std::size_t index, + const TrackNode& node) { + TrackSegmentEndpoints entry; + entry.ref = {kind, index}; + entry.name = node.header.name; + entry.sourceLine = node.header.line; + entry.length = node.length; + + if (const std::optional> ends = + segmentWorldEnds(node.beziers, node.header.originOffset)) { + entry.start = ends->first; + entry.end = ends->second; + entry.hasGeometry = true; + } + + out.push_back(std::move(entry)); +} + +struct EndpointSlot { + std::size_t segmentIndex = 0; + bool isStart = false; +}; + +struct OrientedStep { + std::size_t segmentIndex = 0; + bool reversed = false; +}; + +struct GridKey { + int x = 0; + int z = 0; + + [[nodiscard]] bool operator==(const GridKey& other) const noexcept { + return x == other.x && z == other.z; + } +}; + +struct GridKeyHash { + [[nodiscard]] std::size_t operator()(const GridKey& key) const noexcept { + return static_cast(key.x * 73856093) ^ + static_cast(key.z * 19349663); + } +}; + +struct UnionFind { + std::vector parent; + + explicit UnionFind(const std::size_t size) : parent(size) { + for (std::size_t i = 0; i < size; ++i) { + parent[i] = i; + } + } + + [[nodiscard]] std::size_t find(std::size_t x) { + while (parent[x] != x) { + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + void unite(const std::size_t a, const std::size_t b) { + const std::size_t ra = find(a); + const std::size_t rb = find(b); + if (ra != rb) { + parent[rb] = ra; + } + } +}; + +struct EndpointIndex { + std::vector slots; + std::vector slotAtStart; + std::vector slotAtEnd; + std::vector junctionOfSlot; + std::vector> junctionMembers; +}; + +inline GridKey gridKeyFor(const Vec3& point, const double cellSize) noexcept { + return { + static_cast(std::floor(point.x / cellSize)), + static_cast(std::floor(point.z / cellSize)), + }; +} + +inline void indexEndpoint( + std::unordered_map, GridKeyHash>& grid, + const Vec3& point, + const std::size_t endpointIndex, + const double cellSize) { + const GridKey center = gridKeyFor(point, cellSize); + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + grid[{center.x + dx, center.z + dz}].push_back(endpointIndex); + } + } +} + +inline Vec3 endpointPoint(const TrackSegmentEndpoints& segment, const bool isStart) noexcept { + return isStart ? segment.start : segment.end; +} + +inline EndpointIndex buildEndpointIndex( + const std::vector& segments, + const double tolerance, + const double cellSize) { + EndpointIndex index; + index.slotAtStart.resize(segments.size(), kInvalidSlot); + index.slotAtEnd.resize(segments.size(), kInvalidSlot); + + for (std::size_t segmentIndex = 0; segmentIndex < segments.size(); ++segmentIndex) { + if (!segments[segmentIndex].hasGeometry) { + continue; + } + + index.slotAtStart[segmentIndex] = index.slots.size(); + index.slots.push_back({segmentIndex, true}); + + index.slotAtEnd[segmentIndex] = index.slots.size(); + index.slots.push_back({segmentIndex, false}); + } + + UnionFind uf(index.slots.size()); + std::unordered_map, GridKeyHash> grid; + + for (std::size_t slotIdx = 0; slotIdx < index.slots.size(); ++slotIdx) { + const EndpointSlot& slot = index.slots[slotIdx]; + indexEndpoint( + grid, + endpointPoint(segments[slot.segmentIndex], slot.isStart), + slotIdx, + cellSize); + } + + for (std::size_t left = 0; left < index.slots.size(); ++left) { + const Vec3 leftPoint = + endpointPoint(segments[index.slots[left].segmentIndex], index.slots[left].isStart); + const GridKey center = gridKeyFor(leftPoint, cellSize); + + for (int dz = -1; dz <= 1; ++dz) { + for (int dx = -1; dx <= 1; ++dx) { + const GridKey key{center.x + dx, center.z + dz}; + const auto bucket = grid.find(key); + if (bucket == grid.end()) { + continue; + } + + for (const std::size_t right : bucket->second) { + if (right <= left) { + continue; + } + const Vec3 rightPoint = endpointPoint( + segments[index.slots[right].segmentIndex], + index.slots[right].isStart); + if (distance(leftPoint, rightPoint) <= tolerance) { + uf.unite(left, right); + } + } + } + } + } + + index.junctionOfSlot.resize(index.slots.size()); + index.junctionMembers.resize(index.slots.size()); + for (std::size_t slotIdx = 0; slotIdx < index.slots.size(); ++slotIdx) { + const std::size_t root = uf.find(slotIdx); + index.junctionOfSlot[slotIdx] = root; + index.junctionMembers[root].push_back(slotIdx); + } + + return index; +} + +inline std::size_t slotForExit( + const EndpointIndex& index, + const std::size_t segmentIndex, + const bool exitAtStart) { + return exitAtStart ? index.slotAtStart[segmentIndex] : index.slotAtEnd[segmentIndex]; +} + +inline std::vector neighborsAtExit( + const EndpointIndex& endpointIndex, + const std::size_t segmentIndex, + const bool exitAtStart) { + std::vector steps; + + const std::size_t slot = slotForExit(endpointIndex, segmentIndex, exitAtStart); + if (slot == kInvalidSlot) { + return steps; + } + + const std::size_t junction = endpointIndex.junctionOfSlot[slot]; + for (const std::size_t memberSlot : endpointIndex.junctionMembers[junction]) { + const EndpointSlot& member = endpointIndex.slots[memberSlot]; + if (member.segmentIndex == segmentIndex) { + continue; + } + steps.push_back({member.segmentIndex, !member.isStart}); + } + + return steps; +} + +inline std::size_t greedyChainLength( + const std::size_t segmentIndex, + const bool reversed, + const EndpointIndex& endpointIndex, + std::vector& used) { + if (used[segmentIndex]) { + return 0; + } + + used[segmentIndex] = true; + std::size_t best = 1; + + const bool exitAtStart = reversed; + for (const OrientedStep& step : neighborsAtExit(endpointIndex, segmentIndex, exitAtStart)) { + if (used[step.segmentIndex]) { + continue; + } + best = std::max( + best, + 1 + greedyChainLength(step.segmentIndex, step.reversed, endpointIndex, used)); + } + + used[segmentIndex] = false; + return best; +} + +inline void extendFromExit( + std::vector& links, + std::size_t segmentIndex, + bool exitAtStart, + const EndpointIndex& endpointIndex, + std::vector& used, + const bool prepend) { + while (true) { + std::size_t bestLength = 0; + std::optional bestStep; + + for (const OrientedStep& step : neighborsAtExit(endpointIndex, segmentIndex, exitAtStart)) { + if (used[step.segmentIndex]) { + continue; + } + const std::size_t tail = + greedyChainLength(step.segmentIndex, step.reversed, endpointIndex, used); + if (tail > bestLength) { + bestLength = tail; + bestStep = step; + } + } + + if (!bestStep) { + break; + } + + used[bestStep->segmentIndex] = true; + if (prepend) { + links.insert(links.begin(), {bestStep->segmentIndex, bestStep->reversed}); + } else { + links.push_back({bestStep->segmentIndex, bestStep->reversed}); + } + + segmentIndex = bestStep->segmentIndex; + exitAtStart = bestStep->reversed; + } +} + +inline double routeLengthSum( + const TrackRoute& route, + const std::vector& segments) { + double sum = 0.0; + for (const TrackRouteLink& link : route.links) { + sum += segments[link.segmentIndex].length; + } + return sum; +} + +inline bool routeIsClosed( + const TrackRoute& route, + const std::vector& segments, + const double tolerance) { + if (route.links.size() < 2) { + return false; + } + + const TrackSegmentEndpoints& first = segments[route.links.front().segmentIndex]; + const TrackSegmentEndpoints& last = segments[route.links.back().segmentIndex]; + const Vec3 routeStart = route.links.front().reversed ? first.end : first.start; + const Vec3 routeEnd = route.links.back().reversed ? last.start : last.end; + return distance(routeStart, routeEnd) <= tolerance; +} + +inline TrackRoute buildRouteFromSeed( + const std::size_t seedIndex, + const bool seedReversed, + const EndpointIndex& endpointIndex, + std::vector& used) { + TrackRoute route; + + used[seedIndex] = true; + route.links.push_back({seedIndex, seedReversed}); + + extendFromExit(route.links, seedIndex, seedReversed, endpointIndex, used, false); + extendFromExit(route.links, seedIndex, !seedReversed, endpointIndex, used, true); + + return route; +} + +inline void buildLongestRoutes( + const std::vector& segments, + const EndpointIndex& endpointIndex, + const double tolerance, + std::vector& routesOut) { + std::vector used(segments.size(), false); + + const auto hasUnused = [&]() { + return std::ranges::any_of(used, [](const bool flag) { return !flag; }); + }; + + while (hasUnused()) { + TrackRoute bestRoute; + + for (std::size_t seedIndex = 0; seedIndex < segments.size(); ++seedIndex) { + if (!segments[seedIndex].hasGeometry || used[seedIndex]) { + continue; + } + + for (const bool seedReversed : {false, true}) { + std::vector trialUsed = used; + TrackRoute trial = + buildRouteFromSeed(seedIndex, seedReversed, endpointIndex, trialUsed); + + if (trial.links.size() > bestRoute.links.size()) { + bestRoute = std::move(trial); + } else if ( + trial.links.size() == bestRoute.links.size() && + routeLengthSum(trial, segments) > routeLengthSum(bestRoute, segments)) { + bestRoute = std::move(trial); + } + } + } + + if (bestRoute.links.empty()) { + for (std::size_t i = 0; i < segments.size(); ++i) { + if (segments[i].hasGeometry && !used[i]) { + bestRoute.links.push_back({i, false}); + used[i] = true; + break; + } + } + } else { + for (const TrackRouteLink& link : bestRoute.links) { + used[link.segmentIndex] = true; + } + } + + if (!bestRoute.links.empty()) { + bestRoute.label = segments[bestRoute.links.front().segmentIndex].name; + bestRoute.lengthSum = routeLengthSum(bestRoute, segments); + bestRoute.closed = routeIsClosed(bestRoute, segments, tolerance); + routesOut.push_back(std::move(bestRoute)); + } + } +} + +inline void recordConnections( + const EndpointIndex& endpointIndex, + const std::vector& segments, + std::vector& connectionsOut) { + for (std::size_t left = 0; left < endpointIndex.slots.size(); ++left) { + const std::size_t junctionLeft = endpointIndex.junctionOfSlot[left]; + for (std::size_t right = left + 1; right < endpointIndex.slots.size(); ++right) { + if (endpointIndex.junctionOfSlot[right] != junctionLeft) { + continue; + } + + const EndpointSlot& a = endpointIndex.slots[left]; + const EndpointSlot& b = endpointIndex.slots[right]; + if (a.segmentIndex == b.segmentIndex) { + continue; + } + + const double gap = distance( + endpointPoint(segments[a.segmentIndex], a.isStart), + endpointPoint(segments[b.segmentIndex], b.isStart)); + + connectionsOut.push_back( + {a.segmentIndex, b.segmentIndex, a.isStart, b.isStart, gap}); + } + } +} + +} // namespace detail::track_routes + +[[nodiscard]] inline TrackRouteBuildResult buildTrackRoutes( + const SceneDocument& doc, + const TrackRouteBuildOptions& options = {}) { + TrackRouteBuildResult result; + result.segments.reserve(countTrackInstances(doc)); + + for (std::size_t i = 0; i < doc.nodeTrackNormal.size(); ++i) { + detail::track_routes::appendSegment( + result.segments, TrackSegmentKind::Normal, i, doc.nodeTrackNormal[i]); + } + for (std::size_t i = 0; i < doc.nodeTrackSwitch.size(); ++i) { + detail::track_routes::appendSegment( + result.segments, TrackSegmentKind::Switch, i, doc.nodeTrackSwitch[i]); + } + if (options.includeRoads) { + for (std::size_t i = 0; i < doc.nodeTrackRoad.size(); ++i) { + detail::track_routes::appendSegment( + result.segments, TrackSegmentKind::Road, i, doc.nodeTrackRoad[i]); + } + } + if (options.includeCross) { + for (std::size_t i = 0; i < doc.nodeTrackCross.size(); ++i) { + detail::track_routes::appendSegment( + result.segments, TrackSegmentKind::Cross, i, doc.nodeTrackCross[i]); + } + } + if (options.includeOther) { + for (std::size_t i = 0; i < doc.nodeTrackOther.size(); ++i) { + detail::track_routes::appendSegment( + result.segments, TrackSegmentKind::Other, i, doc.nodeTrackOther[i]); + } + } + + for (const TrackSegmentEndpoints& segment : result.segments) { + if (!segment.hasGeometry) { + ++result.skippedWithoutGeometry; + } + } + + const detail::track_routes::EndpointIndex endpointIndex = + detail::track_routes::buildEndpointIndex( + result.segments, options.endpointTolerance, options.gridCellSize); + + detail::track_routes::recordConnections(endpointIndex, result.segments, result.connections); + + detail::track_routes::buildLongestRoutes( + result.segments, endpointIndex, options.endpointTolerance, result.routes); + + std::ranges::sort(result.routes, [](const TrackRoute& a, const TrackRoute& b) { + if (a.links.size() != b.links.size()) { + return a.links.size() > b.links.size(); + } + return a.lengthSum > b.lengthSum; + }); + + return result; +} + +inline void writeTrackRoutesReport(std::ostream& out, const TrackRouteBuildResult& built) { + out << "# trasy: laczenie wylacznie po geometrii, maksymalna dlugosc lancucha\n"; + out << "# segmenty=" << built.segments.size() << " polaczenia=" << built.connections.size() + << " trasy=" << built.routes.size() << " bez_geometrii=" << built.skippedWithoutGeometry + << '\n'; + + for (std::size_t routeIndex = 0; routeIndex < built.routes.size(); ++routeIndex) { + const TrackRoute& route = built.routes[routeIndex]; + out << "\n[trasa " << (routeIndex + 1) << "] label=" << route.label + << " odcinki=" << route.links.size() << " len_sum=" << route.lengthSum + << " closed=" << (route.closed ? "yes" : "no") << '\n'; + + for (const TrackRouteLink& link : route.links) { + const TrackSegmentEndpoints& segment = built.segments[link.segmentIndex]; + const char* kind = "other"; + switch (segment.ref.kind) { + case TrackSegmentKind::Normal: + kind = "normal"; + break; + case TrackSegmentKind::Switch: + kind = "switch"; + break; + case TrackSegmentKind::Road: + kind = "road"; + break; + case TrackSegmentKind::Cross: + kind = "cross"; + break; + case TrackSegmentKind::Other: + kind = "other"; + break; + } + + out << " " << kind << '[' << segment.ref.index << "] flat=" << link.segmentIndex + << " name=" << segment.name << " L" << (segment.sourceLine + 1) + << " rev=" << (link.reversed ? "yes" : "no") << " len=" << segment.length << '\n'; + } + } +} + +inline void writeTrackRoutesReport( + const std::filesystem::path& outPath, + const TrackRouteBuildResult& built) { + std::ofstream out(outPath); + if (!out) { + throw std::runtime_error("Nie mozna zapisac: " + outPath.string()); + } + writeTrackRoutesReport(out, built); +} + +} // namespace eu07::scene diff --git a/eu07-parser/include/eu07/scene/trainset.hpp b/eu07-parser/include/eu07/scene/trainset.hpp new file mode 100644 index 00000000..e126c373 --- /dev/null +++ b/eu07-parser/include/eu07/scene/trainset.hpp @@ -0,0 +1,50 @@ +#pragma once + +// https://wiki.eu07.pl/index.php?title=Dyrektywa_trainset +// trainset name track offset velocity — tryb scratchpadu do endtrainset. + +#include +#include +#include +#include + +namespace eu07::scene::trainset { + +[[nodiscard]] inline bool parse(TokenStream& stream, ParseContext& ctx) { + if (stream.empty() || !isKeyword(stream.peek().value, "trainset")) { + return false; + } + + if (ctx.scratch.trainset.isOpen) { + ctx.scratch.closeTrainset(); + } + + DirectiveBlock block; + SourceToken head = stream.consume(); + block.line = head.sourceLine; + + std::vector raw; + raw.push_back(std::move(head)); + 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; + } + + block.tokens = std::move(raw); + ctx.scratch.openTrainset(); + ctx.scratch.trainset.name = std::move(name); + ctx.scratch.trainset.track = std::move(track); + ctx.scratch.trainset.offset = offset; + ctx.scratch.trainset.velocity = velocity; + ctx.scratch.trainset.docIndex = ctx.document.trainset.size(); + + ctx.document.trainset.push_back(std::move(block)); + return true; +} + +} // namespace eu07::scene::trainset diff --git a/eu07-parser/include/eu07/scene/wiki.hpp b/eu07-parser/include/eu07/scene/wiki.hpp new file mode 100644 index 00000000..11c26ac1 --- /dev/null +++ b/eu07-parser/include/eu07/scene/wiki.hpp @@ -0,0 +1,53 @@ +#pragma once + +// Indeks dyrektyw: https://wiki.eu07.pl/index.php?title=Plik_scenerii + +namespace eu07::scene::wiki { + +inline constexpr const char* kPlikScenerii = + "https://wiki.eu07.pl/index.php?title=Plik_scenerii"; + +inline constexpr const char* kAtmo = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_atmo"; +inline constexpr const char* kSky = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_sky"; +inline constexpr const char* kTime = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_time"; +inline constexpr const char* kFirstInit = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_FirstInit"; +inline constexpr const char* kTrainset = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_trainset"; +inline constexpr const char* kEvent = + "https://wiki.eu07.pl/index.php?title=Obiekt_event"; +inline constexpr const char* kInclude = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_include"; +inline constexpr const char* kCamera = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_camera"; +inline constexpr const char* kConfig = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_config"; +inline constexpr const char* kLua = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_lua"; +inline constexpr const char* kNode = + "https://wiki.eu07.pl/index.php?title=Obiekt_node"; +inline constexpr const char* kOrigin = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_origin"; +inline constexpr const char* kScale = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_scale"; +inline constexpr const char* kRotate = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_rotate"; +inline constexpr const char* kGroup = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_group"; +inline constexpr const char* kIsolated = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_isolated"; +inline constexpr const char* kArea = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_area"; +inline constexpr const char* kTerrain = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_terrain"; +inline constexpr const char* kDescription = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_description"; +inline constexpr const char* kLight = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_light"; +inline constexpr const char* kTest = + "https://wiki.eu07.pl/index.php?title=Dyrektywa_test"; + +} // namespace eu07::scene::wiki diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index b7c140d0..6c6359ef 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -15,6 +15,9 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "model/AnimModel.h" +#include + +#include "scene/eu7/eu7_pack_bench.h" #include "rendering/renderer.h" #include "model/MdlMngr.h" #include "simulation/simulation.h" @@ -277,6 +280,170 @@ TAnimModel::is_keyword( std::string const &Token ) const { || ( Token == "notransition" ); } +bool +TAnimModel::LoadEu7( + std::string const &model_file, + std::string const &texture_file, + std::vector const &light_states, + std::vector const &light_colors, + bool const transition, + bool const ter ) { + auto name { model_file }; + auto texture { texture_file }; + replace_slashes( name ); + replace_slashes( texture ); + if( false == Init( name, texture ) ) { + if( name != "notload" ) { + if( ter ) { + if( name.ends_with( ".t3d" ) ) { + name[ name.length() - 3 ] = 'e'; + } +#ifdef EU07_USE_OLD_TERRAINCODE + Global.asTerrainModel = name; + WriteLog( "Terrain model \"" + name + "\" will be created." ); +#endif + } + else { + ErrorLog( "Missed file: " + name ); + } + } + return true; + } + + LightsOn[ 0 ] = pModel->GetFromName( "Light_On00" ); + LightsOn[ 1 ] = pModel->GetFromName( "Light_On01" ); + LightsOn[ 2 ] = pModel->GetFromName( "Light_On02" ); + LightsOn[ 3 ] = pModel->GetFromName( "Light_On03" ); + LightsOn[ 4 ] = pModel->GetFromName( "Light_On04" ); + LightsOn[ 5 ] = pModel->GetFromName( "Light_On05" ); + LightsOn[ 6 ] = pModel->GetFromName( "Light_On06" ); + LightsOn[ 7 ] = pModel->GetFromName( "Light_On07" ); + LightsOff[ 0 ] = pModel->GetFromName( "Light_Off00" ); + LightsOff[ 1 ] = pModel->GetFromName( "Light_Off01" ); + LightsOff[ 2 ] = pModel->GetFromName( "Light_Off02" ); + LightsOff[ 3 ] = pModel->GetFromName( "Light_Off03" ); + LightsOff[ 4 ] = pModel->GetFromName( "Light_Off04" ); + LightsOff[ 5 ] = pModel->GetFromName( "Light_Off05" ); + LightsOff[ 6 ] = pModel->GetFromName( "Light_Off06" ); + LightsOff[ 7 ] = pModel->GetFromName( "Light_Off07" ); + sm_winter_variant = pModel->GetFromName( "winter_variant" ); + sm_spring_variant = pModel->GetFromName( "spring_variant" ); + sm_summer_variant = pModel->GetFromName( "summer_variant" ); + sm_autumn_variant = pModel->GetFromName( "autumn_variant" ); + + for( int i = 0; i < iMaxNumLights; ++i ) { + if( LightsOn[ i ] || LightsOff[ i ] ) { + iNumLights = i + 1; + } + } + + for( std::size_t i { 0 }; i < light_states.size() && i < static_cast( iNumLights ); ++i ) { + LightSet( static_cast( i ), light_states[ i ] ); + } + + for( std::size_t i { 0 }; i < light_colors.size() && i < static_cast( iNumLights ); ++i ) { + auto const lightcolor { light_colors[ i ] }; + if( lightcolor != 0xFFFFFFFFu ) { + m_lightcolors[ i ] = { + ( ( lightcolor >> 16 ) & 0xff ) / 255.f, + ( ( lightcolor >> 8 ) & 0xff ) / 255.f, + ( lightcolor & 0xff ) / 255.f }; + } + } + + m_transition = transition; + update_instanceable_flag(); + return true; +} + +bool +TAnimModel::LoadEu7Pack( + std::string const &model_file, + std::string const &texture_file ) { + if( false == Init( model_file, texture_file ) ) { + if( model_file != "notload" ) { + ErrorLog( "Missed file: " + model_file ); + } + return true; + } + + m_eu7_pack = true; + update_instanceable_flag(); + return true; +} + +void +TAnimModel::reset_pack_for_insert( scene::node_data const &nodedata ) { + m_name = nodedata.name; + node_type = nodedata.type; + m_rangesquaredmin = nodedata.range_min * nodedata.range_min; + m_rangesquaredmax = ( + nodedata.range_max >= 0.0 ? + nodedata.range_max * nodedata.range_max : + std::numeric_limits::max() ); + m_visible = true; + m_dirty = false; + m_group = null_handle; + + vAngle = glm::vec3 {}; + m_scale = glm::vec3 { 1.0f, 1.0f, 1.0f }; + pModel = nullptr; + m_eu7_pack = false; + m_instanceable = false; + m_materialdata = {}; + asText.clear(); +} + +namespace pack_instance_pool { + +constexpr std::size_t kChunkSize { 256 }; + +scene::node_data const & +dummy_nodedata() { + static scene::node_data const data { + .range_min = 0.0, + .range_max = -1.0, + .name = {}, + .type = "model" }; + return data; +} + +std::vector> g_owned; +std::vector g_free; + +void +grow_chunk() { + g_owned.reserve( g_owned.size() + kChunkSize ); + g_free.reserve( g_free.size() + kChunkSize ); + for( std::size_t i { 0 }; i < kChunkSize; ++i ) { + auto instance { std::make_unique( dummy_nodedata() ) }; + g_free.push_back( instance.get() ); + g_owned.push_back( std::move( instance ) ); + } +} + +} // namespace pack_instance_pool + +TAnimModel * +TAnimModel::acquire_pack_instance( scene::node_data const &nodedata ) { + if( pack_instance_pool::g_free.empty() ) { + pack_instance_pool::grow_chunk(); + } + + auto *const instance { pack_instance_pool::g_free.back() }; + pack_instance_pool::g_free.pop_back(); + instance->reset_pack_for_insert( nodedata ); + return instance; +} + +void +TAnimModel::release_pack_instance( TAnimModel *const instance ) { + if( instance == nullptr ) { + return; + } + pack_instance_pool::g_free.push_back( instance ); +} + bool TAnimModel::Load(cParser *parser, bool ter) { // rozpoznanie wpisu modelu i ustawienie świateł std::string name = parser->getToken(); @@ -398,7 +565,7 @@ bool TAnimModel::Load(cParser *parser, bool ter) return true; } -namespace { +namespace anim_instancing_detail { // returns true if this animation type mutates per-instance state on the shared // TSubModel tree, which would make batched rendering unsafe. Most animations // loaded from .t3d files are global functions of time (clocks, wind, sky) and @@ -440,7 +607,42 @@ bool submodel_tree_blocks_instancing( TSubModel const *Sub ) { if( submodel_tree_blocks_instancing( Sub->Next ) ) { return true; } return false; } -} // anonymous namespace + +namespace eu7_pack_cache { + +enum class slot_t : std::int8_t { unknown = -1, no = 0, yes = 1 }; + +std::unordered_map g_model; + +[[nodiscard]] bool +lookup( TModel3d *const model, bool &instanceable ) { + auto const it { g_model.find( model ) }; + if( it == g_model.end() || it->second == slot_t::unknown ) { + return false; + } + instanceable = ( it->second == slot_t::yes ); + return true; +} + +void +store( TModel3d *const model, bool const instanceable ) { + g_model[ model ] = instanceable ? slot_t::yes : slot_t::no; +} + +void +warm( TModel3d *const model ) { + if( model == nullptr ) { + return; + } + auto const it { g_model.find( model ) }; + if( it != g_model.end() && it->second != slot_t::unknown ) { + return; + } + store( model, false == submodel_tree_blocks_instancing( model->Root ) ); +} + +} // namespace eu7_pack_cache +} // namespace anim_instancing_detail int TAnimModel::s_instanceable_total = 0; int TAnimModel::s_classified_total = 0; @@ -449,6 +651,54 @@ int TAnimModel::s_rejected_lights = 0; int TAnimModel::s_rejected_animlist = 0; int TAnimModel::s_rejected_animated_submodel = 0; +void +TAnimModel::warm_instanceable_cache( TModel3d *const model ) { + anim_instancing_detail::eu7_pack_cache::warm( model ); +} + +bool +TAnimModel::LoadEu7PackWarm( + TModel3d *const mesh, + std::string const &texture_file ) { + pModel = mesh; + if( mesh == nullptr ) { + return true; + } + + if( + false == texture_file.empty() && + texture_file != "none" && + texture_file.substr( 0, 1 ) != "*" ) { + auto normalized { texture_file }; + replace_slashes( normalized ); + if( + normalized != "none" && + false == normalized.ends_with( "/none" ) && + normalized.find( "tr/none" ) == std::string::npos ) { + m_materialdata.assign( normalized ); + if( m_materialdata.replacable_skins[ 1 ] == null_handle ) { + scene::eu7::pack_bench_inc( &scene::eu7::Eu7PackBench::main_texture_assign_fail ); + scene::eu7::log_pack_texture_fail( normalized ); + } + } + } + + m_eu7_pack = true; + + bool cached_instanceable { false }; + if( anim_instancing_detail::eu7_pack_cache::lookup( mesh, cached_instanceable ) ) { + ++s_classified_total; + if( cached_instanceable ) { + m_instanceable = true; + ++s_instanceable_total; + } + return true; + } + + update_instanceable_flag(); + return true; +} + void TAnimModel::update_instanceable_flag() { // The instanceable path skips RaAnimate() entirely and only calls RaPrepare() // once per bucket. The conditions below ensure that's safe: @@ -463,7 +713,25 @@ void TAnimModel::update_instanceable_flag() { if( pModel == nullptr ) { ++s_rejected_no_pmodel; return; } if( iNumLights != 0 ) { ++s_rejected_lights; return; } if( !m_animlist.empty() ) { ++s_rejected_animlist; return; } - if( submodel_tree_blocks_instancing( pModel->Root ) ) { ++s_rejected_animated_submodel; return; } + + bool cached_instanceable { false }; + if( anim_instancing_detail::eu7_pack_cache::lookup( pModel, cached_instanceable ) ) { + if( false == cached_instanceable ) { + ++s_rejected_animated_submodel; + return; + } + m_instanceable = true; + ++s_instanceable_total; + return; + } + + if( anim_instancing_detail::submodel_tree_blocks_instancing( pModel->Root ) ) { + anim_instancing_detail::eu7_pack_cache::store( pModel, false ); + ++s_rejected_animated_submodel; + return; + } + + anim_instancing_detail::eu7_pack_cache::store( pModel, true ); m_instanceable = true; ++s_instanceable_total; } diff --git a/model/AnimModel.h b/model/AnimModel.h index 13f15a90..4ae624de 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -14,6 +14,9 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include + #include "utilities/Classes.h" #include "utilities/Float3d.h" #include "model/Model3d.h" @@ -118,6 +121,30 @@ public: static void AnimUpdate( double dt ); bool Init(std::string const &asName, std::string const &asReplacableTexture); bool Load(cParser *parser, bool ter = false); + bool LoadEu7( + std::string const &model_file, + std::string const &texture_file, + std::vector const &light_states, + std::vector const &light_colors, + bool transition, + bool ter = false ); + // PACK fast path: mesh already warm in TModelsManager; skips light/season lookups. + bool LoadEu7Pack( + std::string const &model_file, + std::string const &texture_file ); + // PACK stream: mesh pointer already resolved; skips GetModel lookup. + bool LoadEu7PackWarm( + TModel3d *Mesh, + std::string const &texture_file ); + void + reset_pack_for_insert( scene::node_data const &Nodedata ); + static TAnimModel * + acquire_pack_instance( scene::node_data const &Nodedata ); + static void + release_pack_instance( TAnimModel *Instance ); + // Jednorazowa klasyfikacja instancingu per TModel3d (po GetModel na workerze / preload). + static void + warm_instanceable_cache( TModel3d *Model ); std::shared_ptr AddContainer(std::string const &Name); std::shared_ptr GetContainer(std::string const &Name = ""); void LightSet( int const n, float const v ); @@ -215,6 +242,7 @@ public: // no lights, no submodel animations, no replacable skins, no seasonal variants. // computed once at end of Load() so the renderer can simply test the flag. bool m_instanceable { false }; + bool m_eu7_pack { false }; // helper: evaluates current state and updates m_instanceable accordingly. void update_instanceable_flag(); diff --git a/model/MdlMngr.cpp b/model/MdlMngr.cpp index f70ba118..c27d6f74 100644 --- a/model/MdlMngr.cpp +++ b/model/MdlMngr.cpp @@ -120,6 +120,15 @@ TModelsManager::GetModel(std::string const &Name, bool const Dynamic, bool const return model; // NULL jeśli błąd }; +bool +TModelsManager::IsModelCached( std::string const &Name ) { + + std::string filename { Name }; + erase_extension( filename ); + filename = ToLower( filename ); + return find_in_databank( filename ).first; +} + std::pair TModelsManager::find_in_databank( std::string const &Name ) { diff --git a/model/MdlMngr.h b/model/MdlMngr.h index 94baf3d4..03dbe169 100644 --- a/model/MdlMngr.h +++ b/model/MdlMngr.h @@ -23,6 +23,7 @@ class TModelsManager { public: // McZapkie: dodalem sciezke, notabene Path!=Patch :) static TModel3d *GetModel(std::string const &Name, bool const dynamic = false, bool const Logerrors = true , int uid = 0); + [[nodiscard]] static bool IsModelCached( std::string const &Name ); private: // types: diff --git a/model/Model3d.cpp b/model/Model3d.cpp index b483d16e..32c757cd 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -23,6 +23,17 @@ Copyright (C) 2001-2004 Marcin Wozniak, Maciej Czapkiewicz and others #include "simulation/simulationtime.h" #include "world/mtable.h" #include "scene/sn_utils.h" +#include "vehicle/DynObj.h" + +namespace { + +[[nodiscard]] std::string +resolve_material_texture( std::string const &material ) { + auto const resolved { TextureTest( ToLower( material ) ) }; + return resolved.empty() ? material : resolved; +} + +} // namespace //--------------------------------------------------------------------------- @@ -449,7 +460,7 @@ std::pair TSubModel::Load(cParser &parser, bool dynamic) material.insert( 0, Global.asCurrentTexturePath ); } */ - m_material = GfxRenderer->Fetch_Material(material); + m_material = GfxRenderer->Fetch_Material( resolve_material_texture( material ) ); // renderowanie w cyklu przezroczystych tylko jeśli: // 1. Opacity=0 (przejściowo <1, czy tam <100) iFlags |= Opacity < 0.999f ? 0x20 : 0x10; // 0x20-przezroczysta, 0x10-nieprzezroczysta @@ -1474,7 +1485,7 @@ void TSubModel::ReplaceMatrix(const glm::mat4 &mat) void TSubModel::ReplaceMaterial(const std::string &name) { - m_material = GfxRenderer->Fetch_Material(name); + m_material = GfxRenderer->Fetch_Material( resolve_material_texture( name ) ); } // obliczenie maksymalnej wysokości, na początek ślizgu w pantografie @@ -2247,7 +2258,7 @@ void TSubModel::BinInit(TSubModel *s, float4x4 *m, std::vector *t, m_materialname = Global.asCurrentTexturePath + m_materialname; } */ - m_material = GfxRenderer->Fetch_Material(m_materialname); + m_material = GfxRenderer->Fetch_Material( resolve_material_texture( m_materialname ) ); // if we don't have phase flags set for some reason, try to fix it if (!(iFlags & 0x30) && m_material != null_handle) { diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 21c0c487..ee829f2d 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -1,4 +1,4 @@ -/* +/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Timer.h" #include "vehicle/Train.h" #include "vehicle/Camera.h" +#include "scene/eu7/eu7_section_stream.h" #include "simulation/simulation.h" #include "utilities/Logs.h" #include "simulation/simulationtime.h" @@ -450,6 +451,7 @@ bool opengl33_renderer::init_viewport(viewport_config &vp) model_ubo->bind_uniform(); scene_ubo->bind_uniform(); light_ubo->bind_uniform(); + // instance_ubo->bind_uniform(); if (!Global.gfx_skippipeline) { @@ -732,7 +734,9 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode) m_current_viewport = &vp; } - if ((!simulation::is_ready) || (Global.gfx_skiprendering)) + if( ( false == simulation::is_ready ) + || scene::eu7::loading_screen_blocks_world( scene::eu7::stream_loading_position() ) + || Global.gfx_skiprendering ) { gl::framebuffer::unbind(); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); @@ -2794,6 +2798,10 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // still happens inside Render_Instanced(), so correctness is unchanged -- only // the number of calls and glBufferSubData round-trips drops sharply. // (For pickscenery/pickcontrols modes the map stays empty, so this is a no-op.) + // Restore the camera view matrix before flushing: earlier draws in this pass + // (vehicles, section offsets, etc.) may have left stale transforms on the stack. + // OpenGLMatrices.mode( GL_MODELVIEW ); + // OpenGLMatrices.load_matrix( glm::mat4{ glm::mat3{ m_renderpass.pass_camera.modelview() } } ); for( auto const &bucket : m_frame_instance_buckets ) { Render_Instanced( bucket.first.pModel, bucket.second ); } @@ -2818,6 +2826,8 @@ void opengl33_renderer::draw(const gfx::geometry_handle &handle) if( m_current_instance_count > 0 ) { // inside Render_Instanced(): one GL instanced draw replaces what would // otherwise be N regular draws (one per instance) at this submodel + // model_ubo->bind_uniform(); + // instance_ubo->bind_uniform(); m_geometry.draw_instanced( handle, m_current_instance_count ); } else { @@ -3014,6 +3024,11 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector( total - offset_idx, gl::MAX_INSTANCES_PER_BATCH ); // 2a. Upload N modelviews to instance_ubo[0..N-1]. + // instance_ubo->bind_uniform(); instance_ubo->update( reinterpret_cast( m_instance_modelviews.data() + offset_idx ), 0, diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 868e5122..fd7be708 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/color.h" #include "utilities/Globals.h" #include "vehicle/Camera.h" +#include "scene/eu7/eu7_section_stream.h" #include "utilities/Timer.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" @@ -414,9 +415,12 @@ opengl_renderer::Render_pass( rendermode const Mode ) { m_colorpass = m_renderpass; - if( ( !simulation::is_ready ) || ( Global.gfx_skiprendering ) ) { - ::glClearColor( 51.0f / 255.f, 102.0f / 255.f, 85.0f / 255.f, 1.f ); // initial background Color + if( ( false == simulation::is_ready ) + || scene::eu7::loading_screen_blocks_world( scene::eu7::stream_loading_position() ) + || Global.gfx_skiprendering ) { + ::glClearColor( 0.0f, 0.0f, 0.0f, 1.0f ); ::glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + Application.render_ui(); break; } diff --git a/scene/eu7/eu7_apply.cpp b/scene/eu7/eu7_apply.cpp new file mode 100644 index 00000000..995d393a --- /dev/null +++ b/scene/eu7/eu7_apply.cpp @@ -0,0 +1,194 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_apply.h" + +#include "scene/eu7/eu7_loader.h" +#include "scene/eu7/eu7_load_stats.h" +#include "scene/eu7/eu7_transform.h" +#include "simulation/simulation.h" +#include "simulation/simulationstateserializer.h" +#include "utilities/Logs.h" + +namespace scene::eu7 { + +namespace { + +void +insert_mesh_shapes( + basic_region ®ion, + Eu7Module const &module, + scene::scratch_data &scratch ) { + for( auto const &source : module.scene.shapes ) { + shape_node shape { build_shape_node( source ) }; + region.insert( shape, scratch, true ); + } +} + +void +insert_line_shapes( + basic_region ®ion, + Eu7Module const &module, + scene::scratch_data &scratch ) { + for( auto const &source : module.scene.lines ) { + lines_node lines { build_lines_node( source ) }; + region.insert( lines, scratch ); + } +} + +void +transform_segment_path( Eu7SegmentPath &path, Eu7TransformContext const &prefix ) { + path.p_start = transform_point( path.p_start, prefix ); + path.cp_out = transform_vector( path.cp_out, prefix ); + path.cp_in = transform_vector( path.cp_in, prefix ); + path.p_end = transform_point( path.p_end, prefix ); +} + +void +transform_shape_vertices( std::vector &vertices, Eu7TransformContext const &prefix ) { + for( auto &vertex : vertices ) { + vertex.position = transform_point( vertex.position, prefix ); + } +} + +} // namespace + +bool +is_model_only_module( Eu7Module const &module ) { + if( module.has_terrain_chunk ) { + return false; + } + + auto const &scene { module.scene }; + return ( + false == scene.models.empty() && + scene.tracks.empty() && + scene.traction.empty() && + scene.power_sources.empty() && + scene.shapes.empty() && + scene.terrain_shapes.empty() && + scene.lines.empty() && + scene.memcells.empty() && + scene.event_launchers.empty() && + scene.dynamics.empty() && + scene.sounds.empty() && + scene.trainsets.empty() && + scene.events.empty() && + scene.first_init_count == 0 ); +} + +void +compose_models_with_prefix( std::vector &models, Eu7TransformContext const &prefix ) { + if( transform_is_empty( prefix ) ) { + return; + } + + for( auto &model : models ) { + model.location = transform_point( model.location, prefix ); + model.angles.x += prefix.rotation.x; + model.angles.y += prefix.rotation.y; + model.angles.z += prefix.rotation.z; + compose_node_transform( model.node.transform, prefix ); + model.node.area.center = model.location; + } +} + +void +compose_scene_with_include_prefix( Eu7Scene &scene, Eu7TransformContext const &prefix ) { + if( transform_is_empty( prefix ) ) { + return; + } + + for( auto &track : scene.tracks ) { + for( auto &path : track.paths ) { + transform_segment_path( path, prefix ); + } + compose_node_transform( track.node.transform, prefix ); + } + + for( auto &traction : scene.traction ) { + traction.wire_p1 = transform_point( traction.wire_p1, prefix ); + traction.wire_p2 = transform_point( traction.wire_p2, prefix ); + traction.wire_p3 = transform_point( traction.wire_p3, prefix ); + traction.wire_p4 = transform_point( traction.wire_p4, prefix ); + compose_node_transform( traction.node.transform, prefix ); + } + + for( auto &source : scene.power_sources ) { + source.position = transform_point( source.position, prefix ); + compose_node_transform( source.node.transform, prefix ); + } + + for( auto &shape : scene.shapes ) { + shape.origin = transform_point( shape.origin, prefix ); + transform_shape_vertices( shape.vertices, prefix ); + compose_node_transform( shape.node.transform, prefix ); + } + + for( auto &shape : scene.terrain_shapes ) { + shape.origin = transform_point( shape.origin, prefix ); + transform_shape_vertices( shape.vertices, prefix ); + compose_node_transform( shape.node.transform, prefix ); + } + + for( auto &lines : scene.lines ) { + lines.origin = transform_point( lines.origin, prefix ); + transform_shape_vertices( lines.vertices, prefix ); + compose_node_transform( lines.node.transform, prefix ); + } + + compose_models_with_prefix( scene.models, prefix ); + + for( auto &cell : scene.memcells ) { + cell.node.area.center = transform_point( cell.node.area.center, prefix ); + compose_node_transform( cell.node.transform, prefix ); + } + + for( auto &launcher : scene.event_launchers ) { + launcher.location = transform_point( launcher.location, prefix ); + compose_node_transform( launcher.node.transform, prefix ); + launcher.node.area.center = launcher.location; + } + + for( auto &vehicle : scene.dynamics ) { + compose_node_transform( vehicle.node.transform, prefix ); + } + + for( auto &sound : scene.sounds ) { + sound.location = transform_point( sound.location, prefix ); + compose_node_transform( sound.node.transform, prefix ); + sound.node.area.center = sound.location; + } +} + +void +apply_module( + Eu7Module const &module, + simulation::state_serializer &serializer, + scene::scratch_data &scratch ) { + if( simulation::Region != nullptr ) { + { + ScopedTimer const timer { load_stats().terr_ms }; + insert_terrain_shapes( *simulation::Region, module ); + } + { + ScopedTimer const timer { load_stats().mesh_ms }; + insert_mesh_shapes( *simulation::Region, module, scratch ); + } + { + ScopedTimer const timer { load_stats().line_ms }; + insert_line_shapes( *simulation::Region, module, scratch ); + } + } + + serializer.apply_eu7_scene( module.scene, scratch ); +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_apply.h b/scene/eu7/eu7_apply.h new file mode 100644 index 00000000..0ee3aae0 --- /dev/null +++ b/scene/eu7/eu7_apply.h @@ -0,0 +1,37 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +namespace scene { +class scratch_data; +} + +namespace simulation { +class state_serializer; +} + +namespace scene::eu7 { + +[[nodiscard]] bool +is_model_only_module( Eu7Module const &module ); + +void +compose_models_with_prefix( std::vector &models, Eu7TransformContext const &prefix ); + +// Wczytuje zawartosc Eu7Module do symulacji (bez ponownego INCL i bez petli include). +void + apply_module( + Eu7Module const &Module, + simulation::state_serializer &Serializer, + scene::scratch_data &Scratch ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_bake.cpp b/scene/eu7/eu7_bake.cpp new file mode 100644 index 00000000..41334ea3 --- /dev/null +++ b/scene/eu7/eu7_bake.cpp @@ -0,0 +1,337 @@ +/* + +This Source Code Form is subject to the + +terms of the Mozilla Public License, v. + +2.0. If a copy of the MPL was not + +distributed with this file, You can + +obtain one at + +http://mozilla.org/MPL/2.0/. + +*/ + + + +#include "stdafx.h" + +#include "scene/eu7/eu7_bake.h" + + + +#include "scene/eu7/eu7_loader.h" + +#include "utilities/Globals.h" + +#include "utilities/Logs.h" + +#include "utilities/utilities.h" + + + +#include + + + +#ifdef WITH_EU7_PARSER + +#include "scene/eu7/eu7_bake_parser.h" + +#endif + + + +namespace scene::eu7 { + +namespace { + + + +[[nodiscard]] unsigned + +bake_thread_count() { + + if( Global.eu7_bake_threads > 0 ) { + + return static_cast( Global.eu7_bake_threads ); + + } + + auto const hardware { std::thread::hardware_concurrency() }; + + return hardware > 0 ? hardware : 1u; + +} + + + +#ifdef WITH_EU7_PARSER + +[[nodiscard]] bool + +run_scenario_tree_bake( + + std::string const &text_root, + + std::string const &eu7_path, + + Eu7BakeOutcome &outcome ) { + + WriteLog( + + "EU7 bake: generowanie modulow z \"" + text_root + "\" (watek=" + + + std::to_string( bake_thread_count() ) + ")" ); + + + + std::string error; + + if( false == bake_parser::bake_scenario_tree( text_root, bake_thread_count(), error ) ) { + + if( probe_file( eu7_path ) ) { + + ErrorLog( + + "EU7 bake nieudany, uzywam istniejacego modulu: " + error ); + + outcome.ok = true; + + outcome.message = "fallback na stary .eu7 po bledzie bake"; + + return true; + + } + + outcome.message = error; + + return false; + + } + + + + if( false == probe_file( eu7_path ) ) { + + outcome.message = "bake zakonczony, ale brak pliku: " + eu7_path; + + return false; + + } + + + + outcome.ok = true; + + outcome.regenerated = true; + + outcome.message = eu7_path; + + WriteLog( "EU7 bake: zapisano " + eu7_path ); + + return true; + +} + +#endif + + + +} // namespace + + + +bool + +scenario_needs_eu7_regen( std::string const &scenario_file ) { + +#ifdef WITH_EU7_PARSER + + if( false == Global.eu7_auto_bake ) { + + return false; + + } + + + + auto const resolved { resolve_scenery_path( scenario_file ) }; + + + + if( probe_file( resolved ) ) { + + return text_module_is_newer_than_binary( scenario_file ); + + } + + + + if( false == is_text_module_extension( resolved ) || false == FileExists( resolved ) ) { + + return false; + + } + + return false == should_use_binary_module( scenario_file ); + +#else + + (void)scenario_file; + + return false; + +#endif + +} + + + +Eu7BakeOutcome + +ensure_scenario_eu7( std::string const &scenario_file ) { + + Eu7BakeOutcome outcome; + + auto const resolved { resolve_scenery_path( scenario_file ) }; + + auto const eu7_path { resolve_scenery_path( binary_path( scenario_file ) ) }; + + + + if( probe_file( resolved ) ) { + +#ifdef WITH_EU7_PARSER + + if( Global.eu7_auto_bake && text_module_is_newer_than_binary( scenario_file ) ) { + + auto const text_root { text_source_path( scenario_file ) }; + + if( false == text_root.empty() ) { + + WriteLog( + + "EU7: .eu7 nieaktualny wzgledem " + text_root + ", rebake..." ); + + run_scenario_tree_bake( text_root, eu7_path, outcome ); + + return outcome; + + } + + } + +#endif + + outcome.ok = true; + + outcome.message = "uzywam .eu7: " + resolved; + + WriteLog( "EU7: uzywam .eu7: " + resolved ); + + return outcome; + + } + + + +#ifdef WITH_EU7_PARSER + + if( false == Global.eu7_auto_bake ) { + + if( should_use_binary_module( scenario_file ) ) { + + outcome.ok = true; + + outcome.message = "eu7_auto_bake wylaczone, uzywam istniejacego .eu7"; + + return outcome; + + } + + outcome.ok = true; + + outcome.message = "eu7_auto_bake wylaczone, ladowanie SCM"; + + return outcome; + + } + + + + if( false == is_text_module_extension( resolved ) ) { + + outcome.ok = true; + + outcome.message = "nie jest scenariuszem tekstowym"; + + return outcome; + + } + + if( false == FileExists( resolved ) ) { + + outcome.message = "brak pliku scenariusza: " + resolved; + + return outcome; + + } + + + + if( should_use_binary_module( scenario_file ) ) { + + outcome.ok = true; + + outcome.message = "uzywam istniejacego .eu7: " + eu7_path; + + WriteLog( "EU7: uzywam .eu7: " + eu7_path ); + + return outcome; + + } + + + + if( probe_file( eu7_path ) ) { + + WriteLog( + + "EU7: .eu7 nieaktualny wzgledem " + resolved + ", rebake..." ); + + } + + + + run_scenario_tree_bake( resolved, eu7_path, outcome ); + + return outcome; + +#else + + if( should_use_binary_module( scenario_file ) ) { + + outcome.ok = true; + + outcome.message = "uzywam istniejacego .eu7 (parser niedostepny w buildzie)"; + + return outcome; + + } + + outcome.ok = true; + + outcome.message = "parser niedostepny, ladowanie SCM"; + + return outcome; + +#endif + +} + + + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_bake.h b/scene/eu7/eu7_bake.h new file mode 100644 index 00000000..615d4867 --- /dev/null +++ b/scene/eu7/eu7_bake.h @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +namespace scene::eu7 { + +struct Eu7BakeOutcome { + bool ok { false }; + bool regenerated { false }; + std::string message; +}; + +// Czy wejscie to tekst SCM/SCN bez gotowego .eu7 przy wlaczonym auto-bake. +[[nodiscard]] bool +scenario_needs_eu7_regen( std::string const &ScenarioFile ); + +// Przed ladowaniem mapy: uzyj istniejacego .eu7 lub wygeneruj cale drzewo include. +[[nodiscard]] Eu7BakeOutcome +ensure_scenario_eu7( std::string const &ScenarioFile ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_bake_parser.cpp b/scene/eu7/eu7_bake_parser.cpp new file mode 100644 index 00000000..f85683de --- /dev/null +++ b/scene/eu7/eu7_bake_parser.cpp @@ -0,0 +1,170 @@ +/* + +This Source Code Form is subject to the + +terms of the Mozilla Public License, v. + +2.0. If a copy of the MPL was not + +distributed with this file, You can + +obtain one at + +http://mozilla.org/MPL/2.0/. + +*/ + + + +#include + + + +#include + +#include + +#include + + + +namespace scene::eu7::bake_parser { + +namespace { + + + +namespace fs = std::filesystem; + + + +std::mutex g_progress_log_mutex; + + + +void + +log_bake_progress( eu07::scene::bake::BakeProgress const &progress ) { + + std::lock_guard lock { g_progress_log_mutex }; + + auto const path { progress.path.u8string() }; + + switch( progress.phase ) { + + case eu07::scene::bake::BakeProgressPhase::Starting: + + case eu07::scene::bake::BakeProgressPhase::Start: + + break; + + case eu07::scene::bake::BakeProgressPhase::PackModels: + + case eu07::scene::bake::BakeProgressPhase::PackCompose: + + case eu07::scene::bake::BakeProgressPhase::PackComposeDone: + + case eu07::scene::bake::BakeProgressPhase::Bake: + + case eu07::scene::bake::BakeProgressPhase::Done: + + break; + + } + + (void)path; + +} + + + +[[nodiscard]] bool + +is_text_scenery_path( fs::path const &path ) { + + auto const ext { path.extension().string() }; + + return ext == ".scn" || ext == ".scm" || ext == ".inc" || ext == ".sbt"; + +} + + + +} // namespace + + + +bool + +bake_scenario_tree( + + std::string const &text_scenario_path, + + unsigned const max_threads, + + std::string &error_out ) { + + error_out.clear(); + + + + fs::path const input { text_scenario_path }; + + if( false == fs::exists( input ) ) { + + error_out = "brak pliku: " + text_scenario_path; + + return false; + + } + + if( false == is_text_scenery_path( input ) ) { + + error_out = "nie jest plikiem SCM/SCN/INC: " + text_scenario_path; + + return false; + + } + + + + try { + + eu07::scene::bake::BakeTreeOptions options; + + options.maxThreads = max_threads; + + options.onProgress = log_bake_progress; + + + + eu07::scene::bake::BakeTreeStats stats; + + (void)eu07::scene::bake::bakeModuleTree( input, &stats, options ); + + return true; + + } + + catch( std::exception const &ex ) { + + error_out = ex.what(); + + return false; + + } + + catch( ... ) { + + error_out = "nieznany wyjatek bake"; + + return false; + + } + +} + + + +} // namespace scene::eu7::bake_parser + diff --git a/scene/eu7/eu7_bake_parser.h b/scene/eu7/eu7_bake_parser.h new file mode 100644 index 00000000..0e5f557d --- /dev/null +++ b/scene/eu7/eu7_bake_parser.h @@ -0,0 +1,22 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +namespace scene::eu7::bake_parser { + +[[nodiscard]] bool +bake_scenario_tree( + std::string const &TextScenarioPath, + unsigned MaxThreads, + std::string &ErrorOut ); + +} // namespace scene::eu7::bake_parser diff --git a/scene/eu7/eu7_chunks.h b/scene/eu7/eu7_chunks.h new file mode 100644 index 00000000..67c09163 --- /dev/null +++ b/scene/eu7/eu7_chunks.h @@ -0,0 +1,62 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +namespace scene::eu7 { +namespace detail { + +[[nodiscard]] constexpr std::uint32_t +make_id4( char const a, char const b, char const c, char const d ) noexcept { + return ( + ( static_cast( static_cast( d ) ) << 24u ) | + ( static_cast( static_cast( c ) ) << 16u ) | + ( static_cast( static_cast( b ) ) << 8u ) | + static_cast( static_cast( a ) ) ); +} + +} // namespace detail + +constexpr std::uint32_t kEu7Magic { detail::make_id4( 'E', 'U', '7', 'B' ) }; +constexpr std::uint32_t kEu7VersionV4 { 4 }; +constexpr std::uint32_t kEu7VersionV5 { 5 }; +constexpr std::uint32_t kEu7VersionV6 { 6 }; +constexpr std::uint32_t kEu7VersionV7 { 7 }; +constexpr std::uint32_t kEu7VersionV8 { 8 }; + +constexpr std::uint32_t kChunkStrs { detail::make_id4( 'S', 'T', 'R', 'S' ) }; +constexpr std::uint32_t kChunkIncl { detail::make_id4( 'I', 'N', 'C', 'L' ) }; +constexpr std::uint32_t kChunkTrak { detail::make_id4( 'T', 'R', 'A', 'K' ) }; +constexpr std::uint32_t kChunkTrac { detail::make_id4( 'T', 'R', 'A', 'C' ) }; +constexpr std::uint32_t kChunkPwrs { detail::make_id4( 'P', 'W', 'R', 'S' ) }; +constexpr std::uint32_t kChunkTerr { detail::make_id4( 'T', 'E', 'R', 'R' ) }; +constexpr std::uint32_t kChunkMesh { detail::make_id4( 'M', 'E', 'S', 'H' ) }; +constexpr std::uint32_t kChunkLine { detail::make_id4( 'L', 'I', 'N', 'E' ) }; +constexpr std::uint32_t kChunkModl { detail::make_id4( 'M', 'O', 'D', 'L' ) }; +constexpr std::uint32_t kChunkMemc { detail::make_id4( 'M', 'E', 'M', 'C' ) }; +constexpr std::uint32_t kChunkLaun { detail::make_id4( 'L', 'A', 'U', 'N' ) }; +constexpr std::uint32_t kChunkDynm { detail::make_id4( 'D', 'Y', 'N', 'M' ) }; +constexpr std::uint32_t kChunkSond { detail::make_id4( 'S', 'O', 'N', 'D' ) }; +constexpr std::uint32_t kChunkTrset { detail::make_id4( 'T', 'R', 'S', 'E' ) }; +constexpr std::uint32_t kChunkEvnt { detail::make_id4( 'E', 'V', 'N', 'T' ) }; +constexpr std::uint32_t kChunkFint { detail::make_id4( 'F', 'I', 'N', 'T' ) }; +constexpr std::uint32_t kChunkPlac { detail::make_id4( 'P', 'L', 'A', 'C' ) }; + +// Indeks sekcji 1 km -> offset w chunku PACK (EU7B v7+). +constexpr std::uint32_t kChunkPidx { detail::make_id4( 'P', 'I', 'D', 'X' ) }; +// MODL per sekcja, world-space; payload = concat sekcji (offsety z PIDX). +constexpr std::uint32_t kChunkPack { detail::make_id4( 'P', 'A', 'C', 'K' ) }; +// Wspolne definicje modeli (EU7B v8+). +constexpr std::uint32_t kChunkProt { detail::make_id4( 'P', 'R', 'O', 'T' ) }; + +constexpr std::uint8_t kPackSectionFormatV8 { 1 }; + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_emit.cpp b/scene/eu7/eu7_emit.cpp new file mode 100644 index 00000000..330d9369 --- /dev/null +++ b/scene/eu7/eu7_emit.cpp @@ -0,0 +1,790 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_emit.h" +#include "scene/eu7/eu7_transform.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include + +namespace scene::eu7 { +namespace { + +[[nodiscard]] std::string +format_double( double const value ) { + std::string text { std::format( "{:.4f}", value ) }; + if( auto const 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 TextEmitter { +public: + void + push( std::string value ) { + if( !first_on_line_ ) { + out_ << ' '; + } + first_on_line_ = false; + out_ << value; + } + + void + push( std::string_view const value ) { + push( std::string( value ) ); + } + + void + push( char const *const value ) { + push( std::string_view( value ) ); + } + + void + push( double const value ) { + push( format_double( value ) ); + } + + void + push( float const value ) { + push( static_cast( value ) ); + } + + void + push( int const value ) { + push( std::to_string( value ) ); + } + + void + push_vec3( glm::dvec3 const &v ) { + push( v.x ); + push( v.y ); + push( v.z ); + } + + void + newline() { + out_ << '\n'; + first_on_line_ = true; + } + + [[nodiscard]] std::string + str() const { + return out_.str(); + } + +private: + std::ostringstream out_; + bool first_on_line_ { true }; +}; + +struct TransformEmitState { + std::vector origin_stack; + std::vector scale_stack; + glm::dvec3 rotation{}; + std::size_t group_depth = 0; +}; + +[[nodiscard]] glm::dvec3 +origin_push_delta( std::vector const &stack, std::size_t const index ) { + auto const &cumulative { stack[ index ] }; + if( index == 0 ) { + return cumulative; + } + auto const &parent { stack[ index - 1 ] }; + return { cumulative.x - parent.x, cumulative.y - parent.y, cumulative.z - parent.z }; +} + +[[nodiscard]] glm::dvec3 +scale_push_factor( std::vector const &stack, std::size_t const index ) { + auto const &cumulative { stack[ index ] }; + auto const parent { index == 0 ? glm::dvec3{ 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 }; +} + +void +sync_transform_stack( TextEmitter &w, TransformEmitState &state, Eu7TransformContext const &target ) { + while( state.group_depth > target.group_depth ) { + w.push( "endgroup" ); + --state.group_depth; + } + while( state.scale_stack.size() > target.scale_stack.size() ) { + w.push( "endscale" ); + state.scale_stack.pop_back(); + } + while( state.origin_stack.size() > target.origin_stack.size() ) { + w.push( "endorigin" ); + state.origin_stack.pop_back(); + } + + while( state.origin_stack.size() < target.origin_stack.size() ) { + auto const index { state.origin_stack.size() }; + auto const delta { origin_push_delta( target.origin_stack, index ) }; + w.push( "origin" ); + w.push_vec3( delta ); + state.origin_stack.push_back( target.origin_stack[ index ] ); + } + + while( state.scale_stack.size() < target.scale_stack.size() ) { + auto const index { state.scale_stack.size() }; + auto const factor { scale_push_factor( target.scale_stack, index ) }; + w.push( "scale" ); + w.push_vec3( factor ); + state.scale_stack.push_back( target.scale_stack[ index ] ); + } + + while( state.group_depth < target.group_depth ) { + w.push( "group" ); + ++state.group_depth; + } + + if( state.rotation.x != target.rotation.x || state.rotation.y != target.rotation.y || + state.rotation.z != target.rotation.z ) { + w.push( "rotate" ); + w.push_vec3( target.rotation ); + state.rotation = target.rotation; + } +} + +void +emit_node_transform( TextEmitter &w, TransformEmitState &state, Eu7BasicNode const &node ) { + sync_transform_stack( w, state, node.transform ); +} + +[[nodiscard]] double +range_max_from_node( Eu7BasicNode const &node ) { + if( node.range_squared_max >= std::numeric_limits::max() * 0.5 ) { + return -1.0; + } + return std::sqrt( node.range_squared_max ); +} + +[[nodiscard]] double +range_min_from_node( Eu7BasicNode const &node ) { + return std::sqrt( node.range_squared_min ); +} + +[[nodiscard]] std::string +node_display_name( Eu7BasicNode const &node ) { + return node.name.empty() ? "none" : node.name; +} + +void +push_node_header( TextEmitter &w, Eu7BasicNode const &node ) { + w.push( "node" ); + w.push( range_max_from_node( node ) ); + w.push( range_min_from_node( node ) ); + w.push( node_display_name( node ) ); +} + +[[nodiscard]] std::string_view +track_environment_name( Eu7TrackEnvironment const env ) { + switch( env ) { + case Eu7TrackEnvironment::Flat: + return "flat"; + case Eu7TrackEnvironment::Mountains: + return "mountains"; + case Eu7TrackEnvironment::Canyon: + return "canyon"; + case Eu7TrackEnvironment::Tunnel: + return "tunnel"; + case Eu7TrackEnvironment::Bridge: + return "bridge"; + case Eu7TrackEnvironment::Bank: + return "bank"; + default: + return "flat"; + } +} + +[[nodiscard]] std::string_view +track_kind_token( Eu7Track const &track ) { + if( track.category == Eu7TrackCategory::Road ) { + return "road"; + } + if( track.category == Eu7TrackCategory::Water ) { + return "river"; + } + switch( track.track_type ) { + case Eu7TrackType::Switch: + return "switch"; + case Eu7TrackType::Cross: + return "cross"; + case Eu7TrackType::Table: + return "turn"; + case Eu7TrackType::Tributary: + return "tributary"; + default: + return "normal"; + } +} + +void +push_track_core( TextEmitter &w, Eu7Track const &track ) { + w.push( track.length ); + w.push( track.track_width ); + w.push( track.friction ); + w.push( track.sound_distance ); + w.push( track.quality_flag ); + w.push( track.damage_flag ); + w.push( track_environment_name( track.environment ) ); + if( track.visibility ) { + w.push( "vis" ); + w.push( track.visibility->material1 ); + w.push( track.visibility->tex_length ); + w.push( track.visibility->material2 ); + w.push( track.visibility->tex_height1 ); + w.push( track.visibility->tex_width ); + w.push( track.visibility->tex_slope ); + } + else { + w.push( "unvis" ); + } +} + +void +push_track_segments( TextEmitter &w, Eu7Track const &track, Eu7TransformContext const &transform ) { + for( auto const &seg : track.paths ) { + w.push_vec3( subtract_origin_offset( seg.p_start, transform ) ); + w.push( seg.roll_start ); + w.push_vec3( seg.cp_out ); + w.push_vec3( seg.cp_in ); + w.push_vec3( subtract_origin_offset( seg.p_end, transform ) ); + w.push( seg.roll_end ); + w.push( seg.radius ); + } + for( auto const &[key, value] : track.tail_keywords ) { + w.push( key ); + if( key == "sleepermodel" ) { + std::size_t start { 0 }; + while( start < value.size() ) { + auto const 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 ); + } + } +} + +void +emit_track( TextEmitter &w, TransformEmitState &state, Eu7Track const &track ) { + emit_node_transform( w, state, track.node ); + push_node_header( w, track.node ); + w.push( "track" ); + w.push( track_kind_token( track ) ); + push_track_core( w, track ); + push_track_segments( w, track, track.node.transform ); + w.push( "endtrack" ); + w.newline(); +} + +[[nodiscard]] std::string +traction_resistivity_token( Eu7Traction const &traction ) { + if( traction.resistivity_legacy != 0.0 ) { + return format_double( traction.resistivity_legacy ); + } + return format_double( traction.resistivity_ohm_per_m / 0.001 ); +} + +[[nodiscard]] std::string +traction_material_token( Eu7Traction const &traction ) { + if( !traction.material_raw.empty() ) { + return traction.material_raw; + } + switch( traction.material ) { + case Eu7TractionWireMaterial::Aluminium: + return "al"; + case Eu7TractionWireMaterial::None: + return "none"; + default: + return "cu"; + } +} + +void +emit_traction( TextEmitter &w, TransformEmitState &state, Eu7Traction const &traction ) { + emit_node_transform( w, state, traction.node ); + auto const &transform { traction.node.transform }; + push_node_header( w, traction.node ); + w.push( "traction" ); + w.push( traction.power_supply_name ); + w.push( traction.nominal_voltage ); + w.push( traction.max_current ); + w.push( traction_resistivity_token( traction ) ); + w.push( traction_material_token( traction ) ); + w.push( traction.wire_thickness ); + w.push( traction.damage_flag ); + w.push_vec3( subtract_origin_offset( traction.wire_p1, transform ) ); + w.push_vec3( subtract_origin_offset( traction.wire_p2, transform ) ); + w.push_vec3( subtract_origin_offset( traction.wire_p3, transform ) ); + w.push_vec3( subtract_origin_offset( traction.wire_p4, transform ) ); + w.push( traction.min_height ); + w.push( traction.segment_length ); + w.push( traction.wire_count ); + w.push( traction.wire_offset ); + w.push( traction.node.visible ? "vis" : "unvis" ); + if( traction.parallel_name ) { + w.push( "parallel" ); + w.push( *traction.parallel_name ); + } + w.push( "endtraction" ); + w.newline(); +} + +void +emit_power_source( TextEmitter &w, TransformEmitState &state, Eu7TractionPowerSource const &source ) { + emit_node_transform( w, state, source.node ); + push_node_header( w, source.node ); + w.push( "tractionpowersource" ); + w.push_vec3( inverse_transform_point( source.position, source.node.transform ) ); + w.push( source.nominal_voltage ); + w.push( source.voltage_frequency ); + w.push( source.internal_resistance_legacy ); + w.push( source.max_output_current ); + w.push( source.fast_fuse_timeout ); + w.push( source.fast_fuse_repetition ); + w.push( source.slow_fuse_timeout ); + if( source.modifier == Eu7PowerSourceModifier::Recuperation ) { + w.push( "recuperation" ); + } + else if( source.modifier == Eu7PowerSourceModifier::Section ) { + w.push( "section" ); + } + w.push( "end" ); + w.newline(); +} + +void +emit_shape( TextEmitter &w, TransformEmitState &state, Eu7Shape const &shape ) { + emit_node_transform( w, state, shape.node ); + auto local_vertices { shape.vertices }; + inverse_transform_shape_vertices( local_vertices, shape.node.transform ); + push_node_header( w, shape.node ); + auto const shape_type { + shape.node.node_type.empty() ? std::string_view{ "triangles" } : + std::string_view{ shape.node.node_type } }; + w.push( shape_type ); + w.push( shape.material_path ); + auto const triangle_list { shape_type == "triangles" }; + for( std::size_t i { 0 }; i < local_vertices.size(); ++i ) { + auto const &vert { local_vertices[ i ] }; + w.push_vec3( vert.position ); + w.push( static_cast( vert.normal.x ) ); + w.push( static_cast( vert.normal.y ) ); + w.push( static_cast( vert.normal.z ) ); + w.push( vert.u ); + w.push( vert.v ); + if( triangle_list && i % 3 != 2 ) { + w.push( "end" ); + } + } + w.push( "endtri" ); + w.newline(); +} + +void +emit_lines( TextEmitter &w, TransformEmitState &state, Eu7Lines const &lines ) { + emit_node_transform( w, state, lines.node ); + auto local_vertices { lines.vertices }; + inverse_transform_shape_vertices( local_vertices, lines.node.transform ); + push_node_header( 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.line_width ); + for( std::size_t i { 0 }; i + 1 < local_vertices.size(); i += 2 ) { + w.push_vec3( local_vertices[ i ].position ); + w.push_vec3( local_vertices[ i + 1 ].position ); + } + w.push( "endline" ); + w.newline(); +} + +void +emit_model( TextEmitter &w, TransformEmitState &state, Eu7Model const &model ) { + emit_node_transform( w, state, model.node ); + auto const local_location { inverse_transform_point( model.location, model.node.transform ) }; + auto const local_rotation_y { model.angles.y - model.node.transform.rotation.y }; + if( model.is_terrain ) { + w.push( "node" ); + w.push( -1.0 ); + w.push( 0.0 ); + w.push( node_display_name( model.node ) ); + } + else { + push_node_header( w, model.node ); + } + w.push( "model" ); + w.push_vec3( local_location ); + w.push( local_rotation_y ); + w.push( model.model_file ); + w.push( model.texture_file ); + w.push( "endmodel" ); + w.newline(); +} + +void +emit_memcell( TextEmitter &w, TransformEmitState &state, Eu7MemCell const &cell ) { + emit_node_transform( w, state, cell.node ); + push_node_header( w, cell.node ); + w.push( "memcell" ); + w.push_vec3( inverse_transform_point( cell.node.area.center, cell.node.transform ) ); + w.push( cell.text ); + w.push( cell.value1 ); + w.push( cell.value2 ); + w.push( cell.track_name.value_or( "none" ) ); + w.push( "endmemcell" ); + w.newline(); +} + +[[nodiscard]] std::string +launcher_time_token( Eu7EventLauncher const &launcher ) { + if( launcher.launch_hour != 0 || launcher.launch_minute != 0 ) { + return std::format( "{}", launcher.launch_hour * 100 + launcher.launch_minute ); + } + if( launcher.delta_time != 0.0 ) { + return format_double( -launcher.delta_time ); + } + return "0"; +} + +void +emit_launcher( TextEmitter &w, TransformEmitState &state, Eu7EventLauncher const &launcher ) { + emit_node_transform( w, state, launcher.node ); + push_node_header( w, launcher.node ); + w.push( "eventlauncher" ); + w.push_vec3( inverse_transform_point( launcher.location, launcher.node.transform ) ); + w.push( std::sqrt( launcher.radius_squared ) ); + w.push( launcher.activation_key_raw ); + w.push( launcher_time_token( launcher ) ); + w.push( launcher.event1_name ); + if( !launcher.event2_name.empty() && launcher.event2_name != "none" ) { + w.push( launcher.event2_name ); + } + if( launcher.condition ) { + w.push( "condition" ); + w.push( launcher.condition->memcell_name ); + w.push( launcher.condition->compare_text ); + if( launcher.condition->check_mask & 2 ) { + w.push( launcher.condition->compare_value1 ); + } + else { + w.push( "*" ); + } + if( launcher.condition->check_mask & 4 ) { + w.push( launcher.condition->compare_value2 ); + } + else { + w.push( "*" ); + } + } + if( launcher.train_triggered ) { + w.push( "traintriggered" ); + } + w.push( "endeventlauncher" ); + w.newline(); +} + +void +emit_dynamic( TextEmitter &w, TransformEmitState &state, Eu7Dynamic const &vehicle, bool const in_trainset ) { + emit_node_transform( w, state, vehicle.node ); + push_node_header( w, vehicle.node ); + w.push( "dynamic" ); + w.push( vehicle.data_folder ); + w.push( vehicle.skin_file ); + w.push( vehicle.mmd_file ); + if( !in_trainset ) { + w.push( vehicle.track_name ); + } + w.push( vehicle.offset ); + w.push( vehicle.driver_type ); + if( in_trainset ) { + w.push( vehicle.coupling_raw.empty() ? std::to_string( vehicle.coupling ) : vehicle.coupling_raw ); + } + if( !in_trainset ) { + w.push( vehicle.velocity ); + } + w.push( vehicle.load_count ); + if( !vehicle.load_type.empty() ) { + w.push( vehicle.load_type ); + } + if( vehicle.destination ) { + w.push( *vehicle.destination ); + } + w.push( "enddynamic" ); + w.newline(); +} + +void +emit_sound( TextEmitter &w, TransformEmitState &state, Eu7Sound const &sound ) { + emit_node_transform( w, state, sound.node ); + push_node_header( w, sound.node ); + w.push( "sound" ); + w.push_vec3( inverse_transform_point( sound.location, sound.node.transform ) ); + w.push( sound.wav_file ); + w.push( "endsound" ); + w.newline(); +} + +[[nodiscard]] std::string_view +event_type_name( Eu7EventType const type ) { + switch( type ) { + case Eu7EventType::AddValues: + return "addvalues"; + case Eu7EventType::UpdateValues: + return "updatevalues"; + case Eu7EventType::CopyValues: + return "copyvalues"; + case Eu7EventType::GetValues: + return "getvalues"; + case Eu7EventType::PutValues: + return "putvalues"; + case Eu7EventType::Whois: + return "whois"; + case Eu7EventType::LogValues: + return "logvalues"; + case Eu7EventType::Multiple: + return "multiple"; + case Eu7EventType::Switch: + return "switch"; + case Eu7EventType::TrackVel: + return "trackvel"; + case Eu7EventType::Sound: + return "sound"; + case Eu7EventType::Texture: + return "texture"; + case Eu7EventType::Animation: + return "animation"; + case Eu7EventType::Lights: + return "lights"; + case Eu7EventType::Voltage: + return "voltage"; + case Eu7EventType::Visible: + return "visible"; + case Eu7EventType::Friction: + return "friction"; + case Eu7EventType::Message: + return "message"; + default: + return "unknown"; + } +} + +[[nodiscard]] std::string +join_targets( std::vector const &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; +} + +void +emit_event( TextEmitter &w, Eu7Event const &event ) { + w.push( "event" ); + if( event.ignored ) { + w.push( std::string{ "none_" } + event.name ); + } + else { + w.push( event.name ); + } + w.push( event_type_name( event.type ) ); + w.push( event.delay ); + w.push( join_targets( event.targets ) ); + for( auto const &[key, value] : event.payload ) { + if( !key.empty() ) { + w.push( key ); + } + if( !value.empty() ) { + w.push( value ); + } + } + if( event.delay_random != 0.0 ) { + w.push( "randomdelay" ); + w.push( event.delay_random ); + } + if( event.delay_departure != 0.0 ) { + w.push( "departuredelay" ); + w.push( event.delay_departure ); + } + if( event.passive ) { + w.push( "passive" ); + } + w.push( "endevent" ); + w.newline(); +} + +void +emit_trainset_block( + TextEmitter &w, + TransformEmitState &state, + Eu7Trainset const &trainset, + std::vector const &dynamics ) { + w.push( "trainset" ); + w.push( trainset.name ); + w.push( trainset.track ); + w.push( trainset.offset ); + w.push( trainset.velocity ); + if( false == trainset.assignment.empty() ) { + w.push( "assignment" ); + for( auto const &[lang, assignment] : trainset.assignment ) { + w.push( lang ); + w.push( '"' + assignment + '"' ); + } + w.push( "endassignment" ); + } + std::size_t vehicle_slot { 0 }; + for( auto const index : trainset.vehicle_indices ) { + if( index < dynamics.size() ) { + auto vehicle { dynamics[ index ] }; + if( vehicle_slot < trainset.couplings.size() ) { + vehicle.coupling_raw = std::to_string( trainset.couplings[ vehicle_slot ] ); + } + emit_dynamic( w, state, vehicle, true ); + ++vehicle_slot; + } + } + w.push( "endtrainset" ); + w.newline(); +} + +} // namespace + +std::string +include_text_path( Eu7Include const &include ) { + if( !include.source_path.empty() ) { + return include.source_path; + } + auto path { include.binary_path }; + replace_slashes( path ); + if( path.ends_with( ".eu7" ) ) { + path.replace( path.size() - 4, 4, ".inc" ); + } + return path; +} + +std::string +emit_includes_text( std::vector const &includes ) { + if( includes.empty() ) { + return {}; + } + TextEmitter w; + for( auto const &include : includes ) { + w.push( "include" ); + w.push( include_text_path( include ) ); + for( auto const ¶m : include.parameters ) { + w.push( param ); + } + w.push( "end" ); + w.newline(); + } + return w.str(); +} + +std::string +emit_scene_objects_text( Eu7Module const &module ) { + TextEmitter w; + TransformEmitState transform_state; + auto const &scene { module.scene }; + + for( auto const &track : scene.tracks ) { + emit_track( w, transform_state, track ); + } + for( auto const &traction : scene.traction ) { + emit_traction( w, transform_state, traction ); + } + for( auto const &source : scene.power_sources ) { + emit_power_source( w, transform_state, source ); + } + for( auto const &model : scene.models ) { + emit_model( w, transform_state, model ); + } + for( auto const &cell : scene.memcells ) { + emit_memcell( w, transform_state, cell ); + } + for( auto const &launcher : scene.event_launchers ) { + emit_launcher( w, transform_state, launcher ); + } + + std::vector emitted_in_trainset( scene.dynamics.size(), false ); + for( auto const &trainset : scene.trainsets ) { + for( auto const index : trainset.vehicle_indices ) { + if( index < emitted_in_trainset.size() ) { + emitted_in_trainset[ index ] = true; + } + } + } + for( std::size_t i { 0 }; i < scene.dynamics.size(); ++i ) { + if( !emitted_in_trainset[ i ] ) { + emit_dynamic( w, transform_state, scene.dynamics[ i ], false ); + } + } + + for( auto const &sound : scene.sounds ) { + emit_sound( w, transform_state, sound ); + } + + for( auto const &event : scene.events ) { + emit_event( w, event ); + } + + for( std::uint32_t i { 0 }; i < scene.first_init_count; ++i ) { + w.push( "FirstInit" ); + w.newline(); + } + + for( auto const &trainset : scene.trainsets ) { + emit_trainset_block( w, transform_state, trainset, scene.dynamics ); + } + + return w.str(); +} + +std::string +emit_module_text( Eu7Module const &module ) { + TextEmitter w; + TransformEmitState transform_state; + auto const &scene { module.scene }; + + for( auto const &shape : scene.shapes ) { + emit_shape( w, transform_state, shape ); + } + for( auto const &lines : scene.lines ) { + emit_lines( w, transform_state, lines ); + } + + std::string text { w.str() }; + text += emit_scene_objects_text( module ); + return text; +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_emit.h b/scene/eu7/eu7_emit.h new file mode 100644 index 00000000..76e46cea --- /dev/null +++ b/scene/eu7/eu7_emit.h @@ -0,0 +1,34 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include + +namespace scene::eu7 { + +// Sciezka tekstowa dla fallbacku include (source_path albo .eu7 -> .inc). +[[nodiscard]] std::string + include_text_path( Eu7Include const &Include ); + +// Emituje bloki "include ... end" dla parsera SCM. +[[nodiscard]] std::string + emit_includes_text( std::vector const &Includes ); + +// Emituje zawartosc modulu (bez INCL i bez terrain_shapes) do tekstu SCM. +[[nodiscard]] std::string + emit_module_text( Eu7Module const &Module ); + +// Torow, eventy, modele itd. — bez TERR/MESH/LINE (te ida bezposrednio z binarki). +[[nodiscard]] std::string + emit_scene_objects_text( Eu7Module const &Module ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_load_stats.cpp b/scene/eu7/eu7_load_stats.cpp new file mode 100644 index 00000000..2717cb41 --- /dev/null +++ b/scene/eu7/eu7_load_stats.cpp @@ -0,0 +1,148 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_load_stats.h" + +#include "utilities/Logs.h" + +#include +#include + +namespace scene::eu7 { +namespace { + +Eu7LoadStats g_load_stats; + +[[nodiscard]] std::string +format_seconds( double const ms ) { + std::ostringstream out; + out << std::fixed << std::setprecision( 1 ) << ( ms / 1000.0 ) << 's'; + return out.str(); +} + +} // namespace + +double +Eu7LoadStats::total_ms() const { + return ( + read_ms + + scm_fallback_ms + + place_fast_ms + + place_full_ms + + terr_ms + + mesh_ms + + line_ms + + trak_ms + + trac_ms + + power_ms + + model_ms + + memcell_ms + + launcher_ms + + dynamic_ms + + sound_ms + + event_ms + + trainset_ms + + first_init_ms ); +} + +ScopedTimer::ScopedTimer( double &target ) + : m_target { target } + , m_start { std::chrono::steady_clock::now() } {} + +ScopedTimer::~ScopedTimer() { + auto const end { std::chrono::steady_clock::now() }; + m_target += std::chrono::duration( end - m_start ).count(); +} + +Eu7LoadStats & +load_stats() { + return g_load_stats; +} + +void +reset_load_stats() { + g_load_stats = {}; +} + +void +log_load_stats() { + auto const &s { g_load_stats }; + auto const total { s.total_ms() }; + + WriteLog( "EU7 load stats (accounted " + format_seconds( total ) + "):" ); + WriteLog( + " read/cache: " + format_seconds( s.read_ms ) + + " reads=" + std::to_string( s.module_read ) + + " cache_hit=" + std::to_string( s.module_cache_hit ) ); + WriteLog( + " scm fallback: " + format_seconds( s.scm_fallback_ms ) + + " count=" + std::to_string( s.scm_fallback ) ); + WriteLog( + " place fast (MODL-only): " + format_seconds( s.place_fast_ms ) + + " paths=" + std::to_string( s.model_fast_path ) ); + WriteLog( + " place full: " + format_seconds( s.place_full_ms ) + + " paths=" + std::to_string( s.module_full_path ) ); + WriteLog( + " terr: " + format_seconds( s.terr_ms ) ); + WriteLog( + " mesh: " + format_seconds( s.mesh_ms ) ); + WriteLog( + " line: " + format_seconds( s.line_ms ) ); + WriteLog( + " trak: " + format_seconds( s.trak_ms ) + + " count=" + std::to_string( s.tracks ) ); + WriteLog( + " trac: " + format_seconds( s.trac_ms ) + + " count=" + std::to_string( s.traction ) ); + WriteLog( + " power: " + format_seconds( s.power_ms ) + + " count=" + std::to_string( s.power_sources ) ); + WriteLog( + " model insert: " + format_seconds( s.model_ms ) + + " count=" + std::to_string( s.models ) ); + WriteLog( + " memcell: " + format_seconds( s.memcell_ms ) + + " count=" + std::to_string( s.memcells ) ); + WriteLog( + " launcher: " + format_seconds( s.launcher_ms ) + + " count=" + std::to_string( s.launchers ) ); + WriteLog( + " dynamic: " + format_seconds( s.dynamic_ms ) + + " count=" + std::to_string( s.dynamics ) ); + WriteLog( + " sound: " + format_seconds( s.sound_ms ) + + " count=" + std::to_string( s.sounds ) ); + WriteLog( + " event: " + format_seconds( s.event_ms ) + + " count=" + std::to_string( s.events ) ); + WriteLog( + " trainset: " + format_seconds( s.trainset_ms ) + + " count=" + std::to_string( s.trainsets ) ); + WriteLog( + " first_init: " + format_seconds( s.first_init_ms ) ); + if( + s.pack_sections_loaded > 0 || s.pack_skipped_includes > 0 || + s.pack_skipped_models > 0 ) { + WriteLog( + " pack: sections=" + std::to_string( s.pack_sections_loaded ) + + " models=" + std::to_string( s.pack_models ) + + " skipped_incl=" + std::to_string( s.pack_skipped_includes ) + + " skipped_modl=" + std::to_string( s.pack_skipped_models ) ); + } + WriteLog( + " visits=" + std::to_string( s.module_visits ) + + " applied=" + std::to_string( s.module_applied ) + + " deduped=" + std::to_string( s.module_deduped ) ); + WriteLog( + " (roznica vs Scenario loading time = deserialize_continue, map geometry, itd.)" ); +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_load_stats.h b/scene/eu7/eu7_load_stats.h new file mode 100644 index 00000000..d9ce7504 --- /dev/null +++ b/scene/eu7/eu7_load_stats.h @@ -0,0 +1,85 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +namespace scene::eu7 { + +struct Eu7LoadStats { + double read_ms { 0.0 }; + double scm_fallback_ms { 0.0 }; + double place_fast_ms { 0.0 }; + double place_full_ms { 0.0 }; + double terr_ms { 0.0 }; + double mesh_ms { 0.0 }; + double line_ms { 0.0 }; + double trak_ms { 0.0 }; + double trac_ms { 0.0 }; + double power_ms { 0.0 }; + double model_ms { 0.0 }; + double memcell_ms { 0.0 }; + double launcher_ms { 0.0 }; + double dynamic_ms { 0.0 }; + double sound_ms { 0.0 }; + double event_ms { 0.0 }; + double trainset_ms { 0.0 }; + double first_init_ms { 0.0 }; + + std::uint64_t module_visits { 0 }; + std::uint64_t module_applied { 0 }; + std::uint64_t module_deduped { 0 }; + std::uint64_t module_read { 0 }; + std::uint64_t module_cache_hit { 0 }; + std::uint64_t model_fast_path { 0 }; + std::uint64_t module_full_path { 0 }; + std::uint64_t scm_fallback { 0 }; + std::uint64_t models { 0 }; + std::uint64_t tracks { 0 }; + std::uint64_t traction { 0 }; + std::uint64_t power_sources { 0 }; + std::uint64_t memcells { 0 }; + std::uint64_t launchers { 0 }; + std::uint64_t dynamics { 0 }; + std::uint64_t sounds { 0 }; + std::uint64_t events { 0 }; + std::uint64_t trainsets { 0 }; + std::uint64_t pack_skipped_includes { 0 }; + std::uint64_t pack_sections_loaded { 0 }; + std::uint64_t pack_models { 0 }; + std::uint64_t pack_skipped_models { 0 }; + + [[nodiscard]] double total_ms() const; +}; + +class ScopedTimer { +public: + explicit ScopedTimer( double &Target ); + ~ScopedTimer(); + + ScopedTimer( ScopedTimer const & ) = delete; + ScopedTimer &operator=( ScopedTimer const & ) = delete; + +private: + double &m_target; + std::chrono::steady_clock::time_point m_start; +}; + +[[nodiscard]] Eu7LoadStats & +load_stats(); + +void +reset_load_stats(); + +void +log_load_stats(); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_loader.cpp b/scene/eu7/eu7_loader.cpp new file mode 100644 index 00000000..045f68ca --- /dev/null +++ b/scene/eu7/eu7_loader.cpp @@ -0,0 +1,587 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_loader.h" + +#include "scene/eu7/eu7_apply.h" +#include "scene/eu7/eu7_emit.h" +#include "scene/eu7/eu7_load_stats.h" +#include "scene/eu7/eu7_pack_bench.h" +#include "scene/eu7/eu7_reader.h" +#include "scene/eu7/eu7_section_stream.h" +#include "scene/eu7/eu7_parameters.h" +#include "scene/eu7/eu7_transform.h" +#include "scene/scene.h" +#include "scene/scenenode.h" +#include "rendering/renderer.h" +#include "simulation/simulation.h" +#include "simulation/simulationstateserializer.h" +#include "scene/sn_utils.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace scene::eu7 { + +namespace { + +std::unordered_map g_module_file_cache; +bool g_packed_root_active { false }; + +void +apply_material( shape_node::shapenode_data &data, std::string material_name ) { + replace_slashes( material_name ); + data.material = GfxRenderer->Fetch_Material( material_name ); + + auto const texturehandle { + data.material != null_handle ? + GfxRenderer->Material( data.material )->GetTexture( 0 ) : + null_handle }; + auto const &texture { + texturehandle ? + GfxRenderer->Texture( texturehandle ) : + *ITexture::null_texture() }; + if( texturehandle != null_handle ) { + data.translucent = ( + contains( texture.get_name(), '@' ) && texture.get_has_alpha() ); + } + else { + data.translucent = false; + } +} + +void +finalize_shape_bounds( shape_node &shape, shape_node::shapenode_data &data ) { + if( data.vertices.empty() ) { + return; + } + data.area.center = glm::dvec3( 0.0 ); + for( auto const &vertex : data.vertices ) { + data.area.center += vertex.position; + } + data.area.center /= + static_cast( data.vertices.size() ); + shape.invalidate_radius(); +} + +} // namespace + +shape_node +build_shape_node( Eu7Shape const &source ) { + shape_node shape; + auto &data { shape.m_data }; + data.rangesquared_min = source.node.range_squared_min; + data.rangesquared_max = source.node.range_squared_max; + data.visible = source.node.visible; + data.origin = source.origin; + data.lighting.diffuse = glm::vec4( + source.lighting.diffuse.x, source.lighting.diffuse.y, + source.lighting.diffuse.z, source.lighting.diffuse.w ); + data.lighting.ambient = glm::vec4( + source.lighting.ambient.x, source.lighting.ambient.y, + source.lighting.ambient.z, source.lighting.ambient.w ); + data.lighting.specular = glm::vec4( + source.lighting.specular.x, source.lighting.specular.y, + source.lighting.specular.z, source.lighting.specular.w ); + if( source.translucent ) { + data.translucent = true; + } + apply_material( data, source.material_path ); + data.vertices.resize( source.vertices.size() ); + for( std::size_t i { 0 }; i < source.vertices.size(); ++i ) { + auto const &src { source.vertices[ i ] }; + world_vertex &dst { data.vertices[ i ] }; + dst.position = src.position; + dst.normal = src.normal; + dst.texture = glm::vec2( static_cast( src.u ), static_cast( src.v ) ); + } + finalize_shape_bounds( shape, data ); + return shape; +} + +lines_node +build_lines_node( Eu7Lines const &source ) { + lines_node node; + auto &data { node.m_data }; + data.rangesquared_min = source.node.range_squared_min; + data.rangesquared_max = source.node.range_squared_max; + data.visible = source.node.visible; + data.line_width = source.line_width; + data.lighting.diffuse = glm::vec4( + source.lighting.diffuse.x, source.lighting.diffuse.y, + source.lighting.diffuse.z, source.lighting.diffuse.w ); + data.lighting.ambient = glm::vec4( + source.lighting.ambient.x, source.lighting.ambient.y, + source.lighting.ambient.z, source.lighting.ambient.w ); + data.lighting.specular = glm::vec4( + source.lighting.specular.x, source.lighting.specular.y, + source.lighting.specular.z, source.lighting.specular.w ); + data.origin = source.origin; + data.vertices.resize( source.vertices.size() ); + for( std::size_t i { 0 }; i < source.vertices.size(); ++i ) { + data.vertices[ i ].position = source.vertices[ i ].position; + } + node.m_name = source.node.name; + return node; +} + +namespace { + +constexpr std::uint32_t kMagic { MAKE_ID4( 'E', 'U', '7', 'B' ) }; +constexpr std::uint32_t kVersionV4 { 4 }; +constexpr std::uint32_t kVersionV5 { 5 }; +constexpr std::uint32_t kVersionV6 { 6 }; +constexpr std::uint32_t kVersionV7 { 7 }; +constexpr std::uint32_t kVersionV8 { 8 }; +constexpr std::uint32_t kChunkTerr { MAKE_ID4( 'T', 'E', 'R', 'R' ) }; + +std::unordered_set g_load_session; + +[[nodiscard]] std::string +scenery_relative_file( std::string const &resolved ) { + if( resolved.starts_with( Global.asCurrentSceneryPath ) ) { + return resolved.substr( Global.asCurrentSceneryPath.size() ); + } + return resolved; +} + +[[nodiscard]] std::string +include_binary_path( Eu7Include const &include ) { + if( !include.binary_path.empty() ) { + return resolve_scenery_path( include.binary_path ); + } + if( !include.source_path.empty() ) { + return binary_path( include.source_path ); + } + return {}; +} + +void +evict_non_pack_modules_from_cache( std::string const &pack_root_resolved ) { + for( auto it { g_module_file_cache.begin() }; it != g_module_file_cache.end(); ) { + if( it->first != pack_root_resolved ) { + it = g_module_file_cache.erase( it ); + } + else { + ++it; + } + } +} + +bool +load_module_recursive( + std::string const &path, + simulation::state_serializer &serializer, + std::unordered_set &loaded, + Eu7TransformContext const &include_prefix = {}, + std::vector const &include_parameters = {} ) { + ++load_stats().module_visits; + + auto const resolved { resolve_scenery_path( path ) }; + auto const load_key { module_load_key( resolved, include_parameters ) }; + if( loaded.contains( load_key ) ) { + ++load_stats().module_deduped; + return true; + } + if( false == probe_file( resolved ) ) { + return false; + } + + Eu7Module const *template_module { nullptr }; + Eu7Module owned_module; + try { + auto const cached { g_module_file_cache.find( resolved ) }; + if( cached != g_module_file_cache.end() ) { + ++load_stats().module_cache_hit; + template_module = &cached->second; + } + else { + ScopedTimer const read_timer { load_stats().read_ms }; + owned_module = read_module( resolved ); + ++load_stats().module_read; + auto const emplaced { g_module_file_cache.emplace( resolved, std::move( owned_module ) ) }; + template_module = &emplaced.first->second; + } + } + catch( std::exception const &ex ) { + ErrorLog( std::string{ "EU7: blad odczytu \"" + resolved + "\": " } + ex.what() ); + return false; + } + + if( g_packed_root_active && is_model_only_module( *template_module ) ) { + loaded.insert( load_key ); + ++load_stats().pack_skipped_includes; + return true; + } + + loaded.insert( load_key ); + ++load_stats().module_applied; + + if( + false == g_packed_root_active && + include_parameters.empty() && + transform_is_empty( include_prefix ) && + template_module->has_pack_chunk ) { + g_packed_root_active = true; + } + + std::vector fallback_includes; + for( auto const &include : template_module->includes ) { + auto const child { include_binary_path( include ) }; + auto const ref { + include.source_path.empty() ? include.binary_path : include.source_path }; + bool child_loaded { false }; + if( !child.empty() && should_use_binary_module( ref ) ) { + child_loaded = load_module_recursive( + child, + serializer, + loaded, + include.site_transform, + include.parameters ); + } + if( child_loaded ) { + continue; + } + if( !child.empty() && probe_file( child ) && false == should_use_binary_module( ref ) ) { + WriteLog( + "EU7: przestarzaly .eu7, fallback SCM: " + include_text_path( include ) ); + } + else if( !child.empty() ) { + WriteLog( + "EU7 include niedostepny, fallback SCM: " + include_text_path( include ) ); + } + fallback_includes.push_back( include ); + } + + scene::scratch_data scratch; + auto const current_relative { scenery_relative_file( resolved ) }; + if( false == fallback_includes.empty() ) { + ScopedTimer const scm_timer { load_stats().scm_fallback_ms }; + load_stats().scm_fallback += fallback_includes.size(); + for( auto const &include : fallback_includes ) { + serializer.deserialize_include_file( + include_text_path( include ), + current_relative, + include.parameters, + scratch ); + } + } + + if( + fallback_includes.empty() && + is_model_only_module( *template_module ) ) { + ++load_stats().model_fast_path; + + std::vector models { template_module->scene.models }; + { + ScopedTimer const place_timer { load_stats().place_fast_ms }; + if( false == include_parameters.empty() ) { + apply_include_parameters_to_models( models, include_parameters ); + } + if( false == transform_is_empty( include_prefix ) ) { + compose_models_with_prefix( models, include_prefix ); + } + apply_include_placement_to_models( + models, template_module->include_placement, include_parameters ); + } + serializer.apply_eu7_models( models, scratch ); + } + else { + ++load_stats().module_full_path; + + Eu7Module module { *template_module }; + { + ScopedTimer const place_timer { load_stats().place_full_ms }; + if( false == include_parameters.empty() ) { + apply_include_parameters_to_scene( module.scene, include_parameters ); + } + + if( false == transform_is_empty( include_prefix ) ) { + compose_scene_with_include_prefix( module.scene, include_prefix ); + } + + apply_include_placement_to_scene( + module.scene, module.include_placement, include_parameters ); + } + + apply_module( module, serializer, scratch ); + } + + return true; +} + +[[nodiscard]] bool +file_has_terr_chunk( std::string const &path ) { + std::ifstream input { path, std::ios::binary }; + if( !input ) { + return false; + } + + if( sn_utils::ld_uint32( input ) != kMagic ) { + return false; + } + const std::uint32_t version { sn_utils::ld_uint32( input ) }; + if( + version != kVersionV4 && version != kVersionV5 && version != kVersionV6 && + version != kVersionV7 && version != kVersionV8 ) { + return false; + } + + while( input.peek() != EOF ) { + const std::uint32_t chunk_type { sn_utils::ld_uint32( input ) }; + const std::uint32_t chunk_size { sn_utils::ld_uint32( input ) }; + if( chunk_size < 8 ) { + return false; + } + if( chunk_type == kChunkTerr ) { + return true; + } + input.seekg( static_cast( chunk_size - 8 ), std::ios::cur ); + } + return false; +} + +} // namespace + +std::string +resolve_scenery_path( std::string const &reference ) { + auto path { reference }; + while( false == path.empty() && path[ 0 ] == '$' ) { + path.erase( 0, 1 ); + } + replace_slashes( path ); + if( path.starts_with( Global.asCurrentSceneryPath ) ) { + return path; + } + return Global.asCurrentSceneryPath + path; +} + +std::string +binary_path( std::string const &reference ) { + auto path { resolve_scenery_path( reference ) }; + if( path.ends_with( ".scm" ) || path.ends_with( ".sbt" ) || path.ends_with( ".inc" ) || + path.ends_with( ".scn" ) ) { + path.replace( path.size() - 4, 4, ".eu7" ); + } + else if( false == path.ends_with( ".eu7" ) ) { + erase_extension( path ); + path += ".eu7"; + } + return path; +} + +std::string +resolve_parser_include_path( + std::string const &parser_path, + std::string const ¤t_file, + std::string const &include_reference ) { + auto reference { include_reference }; + while( false == reference.empty() && reference[ 0 ] == '$' ) { + reference.erase( 0, 1 ); + } + replace_slashes( reference ); + if( reference.starts_with( Global.asCurrentSceneryPath ) ) { + return reference; + } + if( reference.find( '/' ) != std::string::npos ) { + return parser_path + reference; + } + if( false == current_file.empty() ) { + auto const slash { current_file.find_last_of( '/' ) }; + if( slash != std::string::npos ) { + return parser_path + current_file.substr( 0, slash + 1 ) + reference; + } + } + return parser_path + reference; +} + +std::string +include_eu7_path( + std::string const &parser_path, + std::string const ¤t_file, + std::string const &include_reference ) { + return binary_path( resolve_parser_include_path( parser_path, current_file, include_reference ) ); +} + +std::string +terrain_binary_path( std::string const &terrain_reference ) { + return binary_path( terrain_reference ); +} + +bool +probe_file( std::string const &path ) { + return FileExists( path ) && is_valid_eu7b_file( path ); +} + +bool +probe_baked_scenario( std::string const &scenario_file ) { + return probe_file( resolve_scenery_path( binary_path( scenario_file ) ) ); +} + +bool +is_text_module_extension( std::string const &path ) { + return path.ends_with( ".scn" ) || path.ends_with( ".scm" ) || + path.ends_with( ".inc" ) || path.ends_with( ".sbt" ); +} + +std::string +text_source_path( std::string const &reference ) { + auto const resolved { resolve_scenery_path( reference ) }; + if( is_text_module_extension( resolved ) ) { + return resolved; + } + if( resolved.ends_with( ".eu7" ) ) { + auto stem { resolved }; + stem.erase( stem.size() - 4 ); + for( auto const *ext : { ".scm", ".scn", ".inc", ".sbt" } ) { + auto const candidate { stem + ext }; + if( FileExists( candidate ) ) { + return candidate; + } + } + } + return {}; +} + +bool +text_module_is_newer_than_binary( std::string const &reference ) { + auto const text { text_source_path( reference ) }; + if( text.empty() || false == FileExists( text ) ) { + return false; + } + auto const eu7 { binary_path( reference ) }; + if( false == FileExists( eu7 ) ) { + return true; + } + return last_modified( text ) > last_modified( eu7 ); +} + +bool +should_use_binary_module( std::string const &reference ) { + auto const eu7 { binary_path( reference ) }; + return probe_file( eu7 ) && false == text_module_is_newer_than_binary( reference ); +} + +bool +probe_terrain_file( std::string const &path ) { + return FileExists( path ) && file_has_terr_chunk( path ); +} + +bool +is_scenario_terrain( std::string const &scenario_file ) { + auto stem { scenario_file }; + while( false == stem.empty() && stem[ 0 ] == '$' ) { + stem.erase( 0, 1 ); + } + erase_extension( stem ); + return probe_terrain_file( resolve_scenery_path( stem + ".eu7" ) ); +} + +bool +try_load_scenario_terrain( basic_region ®ion, std::string const &scenario_file ) { + auto stem { scenario_file }; + while( false == stem.empty() && stem[ 0 ] == '$' ) { + stem.erase( 0, 1 ); + } + erase_extension( stem ); + return load_terrain( region, resolve_scenery_path( stem + ".eu7" ) ); +} + +void +insert_terrain_shapes( basic_region ®ion, Eu7Module const &module ) { + scene::scratch_data scratch; + std::size_t shape_count { 0 }; + std::size_t vertex_count { 0 }; + for( auto const &source : module.scene.terrain_shapes ) { + shape_node shape { build_shape_node( source ) }; + vertex_count += source.vertices.size(); + region.insert( shape, scratch, false ); + ++shape_count; + } + if( shape_count > 0 ) { + WriteLog( + "EU7 terrain shapes: count=" + std::to_string( shape_count ) + + " vertices=" + std::to_string( vertex_count ) ); + } +} + +bool +load_terrain( basic_region ®ion, std::string const &path ) { + if( false == probe_terrain_file( path ) ) { + return false; + } + + try { + auto const module { read_module( path ) }; + if( module.scene.terrain_shapes.empty() ) { + ErrorLog( "EU7: brak chunka TERR w \"" + path + "\"" ); + return false; + } + insert_terrain_shapes( region, module ); + WriteLog( "EU7 terrain loaded: " + path ); + return true; + } + catch( std::exception const &ex ) { + ErrorLog( std::string{ "EU7: blad terenu \"" + path + "\": " } + ex.what() ); + return false; + } +} + +void +begin_load_session() { + g_load_session.clear(); + g_packed_root_active = false; + reset_section_stream(); + g_module_file_cache.clear(); + reset_load_stats(); + reset_pack_bench(); +} + +bool +is_module_loaded( std::string const &path ) { + return g_load_session.contains( resolve_scenery_path( path ) ); +} + +bool +pack_scenery_active() { + return g_packed_root_active; +} + +bool +load_module( std::string const &path, simulation::state_serializer &serializer ) { + if( simulation::Region == nullptr ) { + ErrorLog( "EU7: Region nie jest zainicjalizowany" ); + return false; + } + auto const resolved { resolve_scenery_path( path ) }; + auto const ok { load_module_recursive( path, serializer, g_load_session ) }; + if( ok ) { + if( auto const cached { g_module_file_cache.find( resolved ) }; + cached != g_module_file_cache.end() && cached->second.has_pack_chunk ) { + init_section_stream( cached->second, resolved, serializer ); + prime_section_stream( cached->second ); + evict_non_pack_modules_from_cache( resolved ); + } + else { + evict_non_pack_modules_from_cache( {} ); + } + } + return ok; +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_loader.h b/scene/eu7/eu7_loader.h new file mode 100644 index 00000000..02394f2d --- /dev/null +++ b/scene/eu7/eu7_loader.h @@ -0,0 +1,123 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include + +namespace simulation { +class state_serializer; +} + +namespace scene { + +class basic_region; +class shape_node; +class lines_node; + +namespace eu7 { + +// Buduje shape_node z rekordu EU7B (friend shape_node). +[[nodiscard]] shape_node + build_shape_node( Eu7Shape const &Source ); + +// Buduje lines_node z rekordu EU7B (friend lines_node). +[[nodiscard]] lines_node + build_lines_node( Eu7Lines const &Source ); + +// Pełna ścieżka względem Global.asCurrentSceneryPath (jak basic_region::deserialize). +[[nodiscard]] std::string + resolve_scenery_path( std::string const &Reference ); + +// Zamienia .scm/.sbt/.inc/.scn na .eu7 przy tym samym stemie. +[[nodiscard]] std::string + binary_path( std::string const &Reference ); + +// Jak cParser: mPath + include, z katalogiem pliku biezacego dla sciezek wzglednych. +[[nodiscard]] std::string + resolve_parser_include_path( + std::string const &ParserPath, + std::string const &CurrentFile, + std::string const &IncludeReference ); + +// Sciezka .eu7 obok include (ta sama lokalizacja co SCM/INC). +[[nodiscard]] std::string + include_eu7_path( + std::string const &ParserPath, + std::string const &CurrentFile, + std::string const &IncludeReference ); + +// Alias dla terenu (kompatybilnosc wsteczna). +[[nodiscard]] std::string + terrain_binary_path( std::string const &TerrainReference ); + +// Czy plik istnieje i jest poprawnym EU7B (v4-v8). +[[nodiscard]] bool + probe_file( std::string const &Path ); + +// Czy obok scenariusza (.scn/.scm) jest zbakowany plik .eu7. +[[nodiscard]] bool + probe_baked_scenario( std::string const &ScenarioFile ); + +// Rozszerzenia modulow tekstowych (.scn/.scm/.inc/.sbt). +[[nodiscard]] bool + is_text_module_extension( std::string const &Path ); + +// Dla .eu7: istniejacy plik tekstowy obok; dla referencji tekstowej: sciezka rozwiazana. +[[nodiscard]] std::string + text_source_path( std::string const &Reference ); + +// Tekst istnieje i jest nowszy niz .eu7 (lub brak .eu7). +[[nodiscard]] bool + text_module_is_newer_than_binary( std::string const &Reference ); + +// Istniejacy, poprawny .eu7 nie starszy niz odpowiednik tekstowy. +[[nodiscard]] bool + should_use_binary_module( std::string const &Reference ); + +// Czy plik zawiera chunk TERR. +[[nodiscard]] bool + probe_terrain_file( std::string const &Path ); + +// Czy obok scenariusza jest .eu7 z terenem (analogia do .sbt). +[[nodiscard]] bool + is_scenario_terrain( std::string const &ScenarioFile ); + +// Ładuje .eu7 dla danego scenariusza. Zwraca false gdy brak pliku. +[[nodiscard]] bool + try_load_scenario_terrain( basic_region &Region, std::string const &ScenarioFile ); + +// Wczytuje chunk TERR z podanej ścieżki .eu7 do regionu (szybka ścieżka). +[[nodiscard]] bool + load_terrain( basic_region &Region, std::string const &Path ); + +// Wstawia kształty terenu z Eu7Module do regionu. +void + insert_terrain_shapes( basic_region &Region, Eu7Module const &Module ); + +// Czyści sesje deduplikacji przed ladowaniem scenariusza EU7. +void + begin_load_session(); + +// Czy modul juz zostal zaladowany w biezacej sesji (parser include nie laduje drugi raz). +[[nodiscard]] bool + is_module_loaded( std::string const &Path ); + +// Pełne ładowanie modułu EU7B (wszystkie chunki oprócz STRS). +[[nodiscard]] bool + load_module( std::string const &Path, simulation::state_serializer &Serializer ); + +// Root scenariusza ma chunk PACK — modele scenerii ida ze streamingu, nie z MODL w .inc. +[[nodiscard]] bool + pack_scenery_active(); + +} // namespace eu7 +} // namespace scene diff --git a/scene/eu7/eu7_model_prefetch.cpp b/scene/eu7/eu7_model_prefetch.cpp new file mode 100644 index 00000000..2d388db4 --- /dev/null +++ b/scene/eu7/eu7_model_prefetch.cpp @@ -0,0 +1,135 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_model_prefetch.h" + +#include "model/AnimModel.h" +#include "model/MdlMngr.h" +#include "rendering/renderer.h" +#include "scene/eu7/eu7_pack_bench.h" +#include "utilities/utilities.h" +#include "vehicle/DynObj.h" + +#include +#include +#include +#include + +namespace scene::eu7 { +namespace { + +std::mutex g_pack_mesh_load_mutex; +std::unordered_set g_session_warmed_textures; + +[[nodiscard]] bool +pack_texture_usable( std::string texture_file ) { + if( texture_file.empty() || texture_file == "none" || texture_file.front() == '*' ) { + return false; + } + replace_slashes( texture_file ); + if( texture_file == "none" || texture_file.ends_with( "/none" ) ) { + return false; + } + if( texture_file.find( "tr/none" ) != std::string::npos ) { + return false; + } + return true; +} + +void +preload_pack_model_file( std::string const &model_file ) { + if( false == TModelsManager::IsModelCached( model_file ) ) { + return; + } + + if( auto *const mesh { TModelsManager::GetModel( model_file, false, false ) } ) { + TAnimModel::warm_instanceable_cache( mesh ); + } +} + +[[nodiscard]] bool +warm_one_pack_texture( std::string texture_file, std::unordered_set &seen ) { + if( false == pack_texture_usable( texture_file ) ) { + return false; + } + replace_slashes( texture_file ); + if( false == seen.insert( texture_file ).second ) { + return false; + } + if( false == g_session_warmed_textures.insert( texture_file ).second ) { + return false; + } + + auto const resolved { TextureTest( ToLower( texture_file ) ) }; + if( resolved.empty() ) { + pack_bench_inc( &Eu7PackBench::main_texture_warm_miss ); + log_pack_texture_fail( texture_file ); + return false; + } + GfxRenderer->Fetch_Material( resolved ); + + for( int skinindex { 1 }; skinindex <= 4; ++skinindex ) { + auto const multi { + TextureTest( ToLower( texture_file + "," + std::to_string( skinindex ) ) ) }; + if( multi.empty() ) { + break; + } + GfxRenderer->Fetch_Material( multi ); + } + return true; +} + +} // namespace + +void +reset_pack_texture_warm_cache() { + g_session_warmed_textures.clear(); +} + +void +preload_pack_models( std::vector const &models ) { + std::unordered_set seen; + seen.reserve( models.size() ); + + for( auto const &model : models ) { + auto model_file { model.model_file }; + if( model_file.empty() || model_file == "notload" ) { + continue; + } + + replace_slashes( model_file ); + if( false == seen.insert( model_file ).second ) { + continue; + } + + std::lock_guard lock { g_pack_mesh_load_mutex }; + preload_pack_model_file( model_file ); + } +} + +std::size_t +warm_pack_textures_main( Eu7Model const *const models, std::size_t const count ) { + if( models == nullptr || count == 0 || GfxRenderer == nullptr ) { + return 0; + } + + std::unordered_set seen; + seen.reserve( std::min( count, std::size_t { 64 } ) ); + std::size_t warmed { 0 }; + + for( std::size_t i { 0 }; i < count; ++i ) { + if( warm_one_pack_texture( models[ i ].texture_file, seen ) ) { + ++warmed; + } + } + return warmed; +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_model_prefetch.h b/scene/eu7/eu7_model_prefetch.h new file mode 100644 index 00000000..52358b29 --- /dev/null +++ b/scene/eu7/eu7_model_prefetch.h @@ -0,0 +1,33 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include +#include +#include +#include + +namespace scene::eu7 { + +// Tylko warm_instanceable_cache dla juz zcache'owanych meshy (bez cold GetModel na workerze). +void +preload_pack_models( std::vector const &Models ); + +// Main thread: Fetch_Material dla unikalnych texture_file z chunka przed apply. +// Zwraca liczbe unikalnych Fetch_Material w tym slice. +std::size_t +warm_pack_textures_main( Eu7Model const *models, std::size_t count ); + +void +reset_pack_texture_warm_cache(); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_pack_bench.cpp b/scene/eu7/eu7_pack_bench.cpp new file mode 100644 index 00000000..52a0ca11 --- /dev/null +++ b/scene/eu7/eu7_pack_bench.cpp @@ -0,0 +1,405 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_pack_bench.h" + +#include "utilities/Logs.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include +#include +#include + +namespace scene::eu7 { +namespace { + +Eu7PackBench g_pack_bench; +Eu7PackBench g_pack_bench_stream; +Eu7PackBench g_pack_bench_bootstrap; +bool g_stream_phase { false }; +std::unordered_set g_logged_texture_fails; + +struct BenchLine { + std::string label; + double ms { 0.0 }; + std::string detail; +}; + +[[nodiscard]] std::string +format_seconds( double const ms ) { + std::ostringstream out; + out << std::fixed << std::setprecision( 1 ) << ( ms / 1000.0 ) << 's'; + return out.str(); +} + +[[nodiscard]] std::string +format_ms_per( double const ms, std::uint64_t const count ) { + if( count == 0 ) { + return "n/a"; + } + std::ostringstream out; + out << std::fixed << std::setprecision( 2 ) << ( ms / static_cast( count ) ) << " ms/item"; + return out.str(); +} + +void +note_peak( + std::uint64_t Eu7PackBench::*const field, + std::size_t const size ) { + auto &total_peak { g_pack_bench.*field }; + total_peak = std::max( total_peak, size ); + if( g_stream_phase ) { + auto &stream_peak { g_pack_bench_stream.*field }; + stream_peak = std::max( stream_peak, size ); + } +} + +void +add_bench_ms( double Eu7PackBench::*const field, double const delta ) { + g_pack_bench.*field += delta; + if( g_stream_phase ) { + g_pack_bench_stream.*field += delta; + } +} + +void +log_pack_bench_impl( Eu7PackBench const &bench, char const *const title ) { + auto const total { bench.accounted_ms() }; + + if( total <= 0.0 && bench.main_drain_calls == 0 && bench.worker_sections_done == 0 ) { + return; + } + + auto const main_apply_sum { + bench.main_load_eu7_pack_ms + bench.main_load_eu7_full_ms + bench.main_region_insert_ms }; + auto const chunk_sum { + bench.main_chunk_cold_ms + bench.main_chunk_warm_tex_ms + bench.main_chunk_pointer_apply_ms }; + + std::vector lines { + { "worker read_pack", bench.worker_read_pack_ms, + "sections=" + std::to_string( bench.worker_sections_done ) + + " finalized=" + std::to_string( bench.sections_finalized ) + + " models=" + std::to_string( bench.worker_models_decoded ) + + " fail=" + std::to_string( bench.worker_failures ) }, + { "main read_pack", bench.main_read_pack_ms, + "models=" + std::to_string( bench.worker_models_decoded ) }, + { "main GetModel (cold)", bench.main_cold_getmodel_ms, + "calls=" + std::to_string( bench.main_cold_getmodel_calls ) + " " + + format_ms_per( bench.main_cold_getmodel_ms, bench.main_cold_getmodel_calls ) }, + { "main LoadEu7Pack", bench.main_load_eu7_pack_ms, + "loads=" + std::to_string( bench.main_pack_fast_loads ) + " " + + format_ms_per( bench.main_load_eu7_pack_ms, bench.main_pack_fast_loads ) }, + { "main LoadEu7 (full)", bench.main_load_eu7_full_ms, + "loads=" + std::to_string( bench.main_pack_full_loads ) + " " + + format_ms_per( bench.main_load_eu7_full_ms, bench.main_pack_full_loads ) }, + { "main Region::insert", bench.main_region_insert_ms, + "inserts=" + std::to_string( bench.main_region_inserts ) + " " + + format_ms_per( bench.main_region_insert_ms, bench.main_region_inserts ) }, + { "main apply (load+region)", main_apply_sum, + "instances=" + std::to_string( bench.main_instances_applied ) + " " + + format_ms_per( main_apply_sum, bench.main_instances_applied ) }, + { "chunk cold preload", bench.main_chunk_cold_ms, + "loads=" + std::to_string( bench.main_chunk_cold_loads ) + " " + + format_ms_per( bench.main_chunk_cold_ms, bench.main_chunk_cold_loads ) }, + { "chunk warm textures", bench.main_chunk_warm_tex_ms, + "fetches=" + std::to_string( bench.main_chunk_tex_fetches ) + " " + + format_ms_per( bench.main_chunk_warm_tex_ms, bench.main_chunk_tex_fetches ) }, + { "chunk pointer apply", bench.main_chunk_pointer_apply_ms, + "chunks=" + std::to_string( bench.main_chunks ) + " inst=" + + std::to_string( bench.main_chunk_instances ) + " " + + format_ms_per( bench.main_chunk_pointer_apply_ms, bench.main_chunk_instances ) }, + { "chunk wall (phases)", chunk_sum, + "chunks=" + std::to_string( bench.main_chunks ) + " slow8=" + + std::to_string( bench.chunk_slow_8ms ) + " slow16=" + + std::to_string( bench.chunk_slow_16ms ) }, + }; + + std::sort( + lines.begin(), + lines.end(), + []( BenchLine const &lhs, BenchLine const &rhs ) { + return lhs.ms > rhs.ms; + } ); + + WriteLog( std::string{ title } + " (accounted " + format_seconds( total ) + "):" ); + for( auto const &line : lines ) { + if( line.ms <= 0.0 ) { + continue; + } + auto const pct { + total > 0.0 ? + ( 100.0 * line.ms / total ) : + 0.0 }; + std::ostringstream row; + row << std::fixed << std::setprecision( 1 ); + row << " " << line.label << ": " << format_seconds( line.ms ); + if( total > 0.0 ) { + row << " (" << pct << "%)"; + } + if( false == line.detail.empty() ) { + row << " " << line.detail; + } + WriteLog( row.str() ); + } + + WriteLog( + " drain: calls=" + std::to_string( bench.main_drain_calls ) + + " wait_cold_frames=" + std::to_string( bench.main_drain_wait_cold_mesh ) + + " wait_worker_frames=" + std::to_string( bench.main_drain_wait_worker ) + + " idle_frames=" + std::to_string( bench.main_drain_idle ) ); + WriteLog( + " stream: reenqueue=" + std::to_string( bench.stream_reenqueue ) + + " lookahead=" + std::to_string( bench.stream_lookahead_enqueue ) + + " tex_fail=" + std::to_string( bench.main_texture_assign_fail ) + + " warm_miss=" + std::to_string( bench.main_texture_warm_miss ) ); + WriteLog( + " peaks: ready_q=" + std::to_string( bench.peak_ready_queue ) + + " in_flight=" + std::to_string( bench.peak_in_flight ) + + " cold_q=" + std::to_string( bench.peak_pending_cold_meshes ) ); + if( bench.main_chunks > 0 ) { + WriteLog( + " chunk peaks: wall_ms=" + std::to_string( static_cast( bench.peak_chunk_ms ) ) + + " inst=" + std::to_string( bench.peak_chunk_instances ) + + " cold=" + std::to_string( bench.peak_chunk_cold ) + + " last_ms=" + std::to_string( static_cast( bench.last_chunk_ms ) ) + + " last_inst=" + std::to_string( bench.last_chunk_instances ) + + " budget_stops=" + std::to_string( bench.drain_budget_stops ) ); + } + + if( false == lines.empty() && lines.front().ms > 0.0 ) { + WriteLog( " >> bottleneck: " + lines.front().label ); + } +} + +} // namespace + +double +Eu7PackBench::accounted_ms() const { + return ( + worker_read_pack_ms + + main_read_pack_ms + + main_cold_getmodel_ms + + main_load_eu7_pack_ms + + main_load_eu7_full_ms + + main_region_insert_ms ); +} + +double +Eu7PackBench::chunk_accounted_ms() const { + return ( + main_chunk_cold_ms + + main_chunk_warm_tex_ms + + main_chunk_pointer_apply_ms ); +} + +PackBenchTimer::PackBenchTimer( double Eu7PackBench::*const field ) + : m_field { field } + , m_start { std::chrono::steady_clock::now() } {} + +PackBenchTimer::~PackBenchTimer() { + auto const end { std::chrono::steady_clock::now() }; + auto const delta { + std::chrono::duration( end - m_start ).count() }; + g_pack_bench.*m_field += delta; + if( g_stream_phase ) { + g_pack_bench_stream.*m_field += delta; + } +} + +Eu7PackBench & +pack_bench() { + return g_pack_bench; +} + +Eu7PackBench & +pack_bench_stream() { + return g_pack_bench_stream; +} + +Eu7PackBench const & +pack_bench_bootstrap() { + return g_pack_bench_bootstrap; +} + +bool +pack_bench_stream_phase_active() { + return g_stream_phase; +} + +void +reset_pack_bench() { + g_pack_bench = {}; + g_pack_bench_stream = {}; + g_pack_bench_bootstrap = {}; + g_stream_phase = false; + g_logged_texture_fails.clear(); +} + +void +pack_bench_begin_stream_phase() { + g_pack_bench_bootstrap = g_pack_bench; + g_pack_bench_stream = {}; + g_stream_phase = true; +} + +void +pack_bench_note_ready_queue( std::size_t const size ) { + note_peak( &Eu7PackBench::peak_ready_queue, size ); +} + +void +pack_bench_note_in_flight( std::size_t const size ) { + note_peak( &Eu7PackBench::peak_in_flight, size ); +} + +void +pack_bench_note_pending_cold_meshes( std::size_t const size ) { + note_peak( &Eu7PackBench::peak_pending_cold_meshes, size ); +} + +void +pack_bench_inc( std::uint64_t Eu7PackBench::*const field, std::uint64_t const amount ) { + g_pack_bench.*field += amount; + if( g_stream_phase ) { + g_pack_bench_stream.*field += amount; + } +} + +void +pack_bench_note_chunk( + double const wall_ms, + std::size_t const instances, + std::size_t const cold_meshes, + double const cold_ms, + double const warm_tex_ms, + double const pointer_apply_ms ) { + add_bench_ms( &Eu7PackBench::main_chunk_cold_ms, cold_ms ); + add_bench_ms( &Eu7PackBench::main_chunk_warm_tex_ms, warm_tex_ms ); + add_bench_ms( &Eu7PackBench::main_chunk_pointer_apply_ms, pointer_apply_ms ); + pack_bench_inc( &Eu7PackBench::main_chunks ); + pack_bench_inc( &Eu7PackBench::main_chunk_instances, instances ); + pack_bench_inc( &Eu7PackBench::main_chunk_cold_loads, cold_meshes ); + + g_pack_bench.last_chunk_ms = wall_ms; + g_pack_bench.last_chunk_instances = instances; + g_pack_bench.last_chunk_cold = cold_meshes; + if( wall_ms > g_pack_bench.peak_chunk_ms ) { + g_pack_bench.peak_chunk_ms = wall_ms; + g_pack_bench.peak_chunk_instances = instances; + g_pack_bench.peak_chunk_cold = cold_meshes; + } + if( wall_ms >= 8.0 ) { + pack_bench_inc( &Eu7PackBench::chunk_slow_8ms ); + } + if( wall_ms >= 16.0 ) { + pack_bench_inc( &Eu7PackBench::chunk_slow_16ms ); + } + + if( false == g_stream_phase ) { + return; + } + + auto &stream { g_pack_bench_stream }; + stream.last_chunk_ms = wall_ms; + stream.last_chunk_instances = instances; + stream.last_chunk_cold = cold_meshes; + if( wall_ms > stream.peak_chunk_ms ) { + stream.peak_chunk_ms = wall_ms; + stream.peak_chunk_instances = instances; + stream.peak_chunk_cold = cold_meshes; + } +} + +void +log_pack_texture_fail( std::string const &texture_path ) { + if( texture_path.empty() ) { + return; + } + + auto key { texture_path }; + replace_slashes( key ); + key = ToLower( key ); + if( false == g_logged_texture_fails.insert( key ).second ) { + return; + } + + WriteLog( "EU7 PACK tex_fail: \"" + texture_path + "\"" ); +} + +void +log_pack_bench() { + if( g_pack_bench_bootstrap.worker_sections_done > 0 + || g_pack_bench_bootstrap.accounted_ms() > 0.0 ) { + log_pack_bench_impl( g_pack_bench_bootstrap, "EU7 PACK bench [bootstrap]" ); + return; + } + log_pack_bench_impl( g_pack_bench, "EU7 PACK bench" ); +} + +void +log_pack_stream_bench() { + log_pack_bench_impl( g_pack_bench_stream, "EU7 PACK bench [stream]" ); +} + +void +flush_pack_stream_bench() { + if( false == g_stream_phase ) { + return; + } + if( + g_pack_bench_stream.sections_finalized == 0 && + g_pack_bench_stream.main_drain_calls == 0 && + g_pack_bench_stream.accounted_ms() <= 0.0 ) { + return; + } + log_pack_stream_bench(); +} + +void +log_pack_stream_status( + glm::dvec3 const &camera_position, + double const camera_speed_mps, + float const inner_ring_progress, + float const outer_ring_progress, + std::size_t const ready_queue, + std::size_t const in_flight, + std::size_t const pending_offset, + std::size_t const pending_total, + double const drain_budget_ms, + std::size_t const chunk_limit ) { + auto const &bench { g_pack_bench_stream }; + WriteLog( + "EU7 PACK [live]: cam=" + std::to_string( camera_position.x ) + "," + + std::to_string( camera_position.z ) + " m/s=" + std::to_string( camera_speed_mps ) + + " ring4=" + std::to_string( static_cast( inner_ring_progress * 100.f ) ) + "%" + + " ring11=" + std::to_string( static_cast( outer_ring_progress * 100.f ) ) + "%" + + " ready=" + std::to_string( ready_queue ) + + " in_flight=" + std::to_string( in_flight ) + + " pending=" + std::to_string( pending_offset ) + "/" + std::to_string( pending_total ) + + " slice=" + std::to_string( chunk_limit ) + + " budget_ms=" + std::to_string( static_cast( drain_budget_ms ) ) + + " applied=" + std::to_string( bench.main_instances_applied ) + + " tex_fail=" + std::to_string( bench.main_texture_assign_fail ) + + " warm_miss=" + std::to_string( bench.main_texture_warm_miss ) + + " chunks=" + std::to_string( bench.main_chunks ) + + " last_chunk_ms=" + std::to_string( static_cast( bench.last_chunk_ms ) ) + + " peak_chunk_ms=" + std::to_string( static_cast( bench.peak_chunk_ms ) ) + + " slow8=" + std::to_string( bench.chunk_slow_8ms ) + + " cold_ms=" + std::to_string( static_cast( bench.main_chunk_cold_ms ) ) + + " warm_ms=" + std::to_string( static_cast( bench.main_chunk_warm_tex_ms ) ) + + " apply_ms=" + std::to_string( static_cast( bench.main_chunk_pointer_apply_ms ) ) + + " reenq=" + std::to_string( bench.stream_reenqueue ) + + " ahead=" + std::to_string( bench.stream_lookahead_enqueue ) ); +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_pack_bench.h b/scene/eu7/eu7_pack_bench.h new file mode 100644 index 00000000..0948adc0 --- /dev/null +++ b/scene/eu7/eu7_pack_bench.h @@ -0,0 +1,159 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +#include + +namespace scene::eu7 { + +// Liczniki i czasy PACK — osobno od Eu7LoadStats (deserialize / SCM). +struct Eu7PackBench { + // --- worker (async read PACK) --- + double worker_read_pack_ms { 0.0 }; + std::uint64_t worker_sections_done { 0 }; + std::uint64_t sections_finalized { 0 }; + std::uint64_t worker_models_decoded { 0 }; + std::uint64_t worker_failures { 0 }; + + // --- main thread: apply --- + double main_read_pack_ms { 0.0 }; + double main_cold_getmodel_ms { 0.0 }; + double main_load_eu7_pack_ms { 0.0 }; + double main_load_eu7_full_ms { 0.0 }; + double main_region_insert_ms { 0.0 }; + + std::uint64_t main_drain_calls { 0 }; + std::uint64_t main_cold_getmodel_calls { 0 }; + std::uint64_t main_instances_applied { 0 }; + std::uint64_t main_pack_fast_loads { 0 }; + std::uint64_t main_pack_full_loads { 0 }; + std::uint64_t main_region_inserts { 0 }; + + std::uint64_t main_drain_wait_cold_mesh { 0 }; + std::uint64_t main_drain_wait_worker { 0 }; + std::uint64_t main_drain_idle { 0 }; + + std::uint64_t peak_ready_queue { 0 }; + std::uint64_t peak_in_flight { 0 }; + std::uint64_t peak_pending_cold_meshes { 0 }; + + std::uint64_t main_texture_assign_fail { 0 }; + std::uint64_t main_texture_warm_miss { 0 }; + std::uint64_t stream_reenqueue { 0 }; + std::uint64_t stream_lookahead_enqueue { 0 }; + + // --- main: pointer chunk apply (diag zwiechy) --- + double main_chunk_cold_ms { 0.0 }; + double main_chunk_warm_tex_ms { 0.0 }; + double main_chunk_pointer_apply_ms { 0.0 }; + std::uint64_t main_chunks { 0 }; + std::uint64_t main_chunk_instances { 0 }; + std::uint64_t main_chunk_cold_loads { 0 }; + std::uint64_t main_chunk_tex_fetches { 0 }; + std::uint64_t chunk_slow_8ms { 0 }; + std::uint64_t chunk_slow_16ms { 0 }; + std::uint64_t drain_budget_stops { 0 }; + double peak_chunk_ms { 0.0 }; + std::uint64_t peak_chunk_instances { 0 }; + std::uint64_t peak_chunk_cold { 0 }; + double last_chunk_ms { 0.0 }; + std::uint64_t last_chunk_instances { 0 }; + std::uint64_t last_chunk_cold { 0 }; + + [[nodiscard]] double accounted_ms() const; + [[nodiscard]] double chunk_accounted_ms() const; +}; + +class PackBenchTimer { +public: + explicit PackBenchTimer( double Eu7PackBench::*Field ); + ~PackBenchTimer(); + + PackBenchTimer( PackBenchTimer const & ) = delete; + PackBenchTimer &operator=( PackBenchTimer const & ) = delete; + +private: + double Eu7PackBench::*const m_field; + std::chrono::steady_clock::time_point m_start; +}; + +[[nodiscard]] Eu7PackBench & +pack_bench(); + +// Po dismiss overlay — liczniki runtime streamingu (latanie kamera / jazda). +[[nodiscard]] Eu7PackBench & +pack_bench_stream(); + +// Snapshot z fazy bootstrap (ekran ladowania). +[[nodiscard]] Eu7PackBench const & +pack_bench_bootstrap(); + +[[nodiscard]] bool +pack_bench_stream_phase_active(); + +void +reset_pack_bench(); + +// Wywolac raz przy dismiss overlay — oddziela bootstrap od streamu. +void +pack_bench_begin_stream_phase(); + +void +pack_bench_note_ready_queue( std::size_t Size ); + +void +pack_bench_note_in_flight( std::size_t Size ); + +void +pack_bench_note_pending_cold_meshes( std::size_t Size ); + +void +pack_bench_inc( std::uint64_t Eu7PackBench::*Field, std::uint64_t Amount = 1 ); + +void +pack_bench_note_chunk( + double WallMs, + std::size_t Instances, + std::size_t ColdMeshes, + double ColdMs, + double WarmTexMs, + double PointerApplyMs ); + +void +log_pack_texture_fail( std::string const &TexturePath ); + +void +log_pack_bench(); + +void +log_pack_stream_bench(); + +// Na wyjsciu z jazdy / reset sceny — jednorazowy dump bez czekania na timer. +void +flush_pack_stream_bench(); + +// Co kilka sekund / przy szybkiej kamerze — stan kolejek i ringu. +void +log_pack_stream_status( + glm::dvec3 const &CameraPosition, + double CameraSpeedMps, + float InnerRingProgress, + float OuterRingProgress, + std::size_t ReadyQueue, + std::size_t InFlight, + std::size_t PendingOffset, + std::size_t PendingTotal, + double DrainBudgetMs, + std::size_t ChunkLimit ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_parameters.cpp b/scene/eu7/eu7_parameters.cpp new file mode 100644 index 00000000..a74d792d --- /dev/null +++ b/scene/eu7/eu7_parameters.cpp @@ -0,0 +1,282 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_parameters.h" +#include "scene/eu7/eu7_apply.h" +#include "scene/eu7/eu7_transform.h" + +#include +#include +#include +#include + +namespace scene::eu7 { +namespace { + +void +bind_include_parameter_impl( std::string &text, std::vector const ¶meters, bool const to_lower ) { + if( parameters.empty() ) { + return; + } + + std::size_t pos { 0 }; + while( ( pos = text.find( "(p", pos ) ) != std::string::npos ) { + auto const close { text.find( ')', pos ) }; + if( close == std::string::npos ) { + break; + } + + auto const index_text { text.substr( pos + 2, close - ( pos + 2 ) ) }; + text.erase( pos, ( close - pos ) + 1 ); + + auto const index { static_cast( std::atoi( index_text.c_str() ) ) }; + auto const replacement { ( + index >= 1 && ( index - 1 ) < parameters.size() ) ? + parameters[ index - 1 ] : + std::string{ "none" } }; + + text.insert( pos, replacement ); + + if( to_lower ) { + for( std::size_t i { pos }; i < pos + replacement.size(); ++i ) { + text[ i ] = static_cast( tolower( static_cast( text[ i ] ) ) ); + } + } + + pos += replacement.size(); + } +} + +void +bind_name( std::string &text, std::vector const ¶meters ) { + bind_include_parameter_impl( text, parameters, true ); +} + +void +bind_path( std::string &text, std::vector const ¶meters ) { + bind_include_parameter_impl( text, parameters, false ); +} + +void +bind_optional_name( std::optional &text, std::vector const ¶meters ) { + if( text ) { + bind_name( *text, parameters ); + } +} + +void +bind_node_strings( Eu7BasicNode &node, std::vector const ¶meters ) { + bind_name( node.name, parameters ); + bind_name( node.node_type, parameters ); +} + +} // namespace + +void +bind_include_parameter( std::string &text, std::vector const ¶meters, bool const to_lower ) { + bind_include_parameter_impl( text, parameters, to_lower ); +} + +void +apply_include_parameters_to_models( std::vector &models, std::vector const ¶meters ) { + if( parameters.empty() ) { + return; + } + + for( auto &model : models ) { + bind_node_strings( model.node, parameters ); + bind_path( model.model_file, parameters ); + bind_path( model.texture_file, parameters ); + } +} + +void +apply_include_parameters_to_scene( Eu7Scene &scene, std::vector const ¶meters ) { + if( parameters.empty() ) { + return; + } + + for( auto &track : scene.tracks ) { + bind_node_strings( track.node, parameters ); + if( track.visibility ) { + bind_path( track.visibility->material1, parameters ); + bind_path( track.visibility->material2, parameters ); + } + for( auto &[key, value] : track.tail_keywords ) { + bind_name( key, parameters ); + bind_name( value, parameters ); + } + } + + for( auto &traction : scene.traction ) { + bind_node_strings( traction.node, parameters ); + bind_name( traction.power_supply_name, parameters ); + bind_name( traction.material_raw, parameters ); + bind_optional_name( traction.parallel_name, parameters ); + } + + for( auto &source : scene.power_sources ) { + bind_node_strings( source.node, parameters ); + } + + for( auto &shape : scene.shapes ) { + bind_node_strings( shape.node, parameters ); + bind_path( shape.material_path, parameters ); + } + + for( auto &shape : scene.terrain_shapes ) { + bind_node_strings( shape.node, parameters ); + bind_path( shape.material_path, parameters ); + } + + for( auto &lines : scene.lines ) { + bind_node_strings( lines.node, parameters ); + } + + apply_include_parameters_to_models( scene.models, parameters ); + + for( auto &cell : scene.memcells ) { + bind_node_strings( cell.node, parameters ); + bind_name( cell.text, parameters ); + bind_optional_name( cell.track_name, parameters ); + } + + for( auto &launcher : scene.event_launchers ) { + bind_node_strings( launcher.node, parameters ); + bind_name( launcher.activation_key_raw, parameters ); + bind_name( launcher.event1_name, parameters ); + bind_name( launcher.event2_name, parameters ); + if( launcher.condition ) { + bind_name( launcher.condition->memcell_name, parameters ); + bind_name( launcher.condition->compare_text, parameters ); + } + } + + for( auto &vehicle : scene.dynamics ) { + bind_node_strings( vehicle.node, parameters ); + bind_path( vehicle.data_folder, parameters ); + bind_path( vehicle.skin_file, parameters ); + bind_path( vehicle.mmd_file, parameters ); + bind_name( vehicle.track_name, parameters ); + bind_name( vehicle.driver_type, parameters ); + bind_name( vehicle.coupling_raw, parameters ); + bind_path( vehicle.coupling_params, parameters ); + bind_name( vehicle.load_type, parameters ); + bind_optional_name( vehicle.destination, parameters ); + } + + for( auto &sound : scene.sounds ) { + bind_node_strings( sound.node, parameters ); + bind_path( sound.wav_file, parameters ); + } + + for( auto &event : scene.events ) { + bind_name( event.name, parameters ); + for( auto &target : event.targets ) { + bind_name( target, parameters ); + } + for( auto &[key, value] : event.payload ) { + bind_name( key, parameters ); + bind_name( value, parameters ); + } + } + + for( auto &trainset : scene.trainsets ) { + bind_name( trainset.name, parameters ); + bind_name( trainset.track, parameters ); + std::unordered_map rebound; + rebound.reserve( trainset.assignment.size() ); + for( auto const &[key, value] : trainset.assignment ) { + auto bound_key { key }; + auto bound_value { value }; + bind_name( bound_key, parameters ); + bind_name( bound_value, parameters ); + rebound.emplace( std::move( bound_key ), std::move( bound_value ) ); + } + trainset.assignment = std::move( rebound ); + } +} + +[[nodiscard]] double +resolve_placement_param( + std::uint8_t const param_index, + std::vector const ¶meters ) { + if( param_index == 0 ) { + return 0.0; + } + + auto const index { static_cast( param_index ) }; + if( index < 1 || index > parameters.size() ) { + throw std::runtime_error( "EU7: brak parametru p" + std::to_string( param_index ) ); + } + + return std::stod( parameters[ index - 1 ] ); +} + +Eu7TransformContext +placement_transform_from_include_parameters( + Eu7IncludePlacement const &binding, + std::vector const ¶meters ) { + Eu7TransformContext placement; + if( binding.empty() ) { + return placement; + } + + try { + auto const origin_x { resolve_placement_param( binding.origin_x_param, parameters ) }; + auto const origin_y { resolve_placement_param( binding.origin_y_param, parameters ) }; + auto const origin_z { resolve_placement_param( binding.origin_z_param, parameters ) }; + placement.origin_stack.push_back( { origin_x, origin_y, origin_z } ); + placement.rotation.y = resolve_placement_param( binding.rotation_y_param, parameters ); + } + catch( std::exception const & ) { + placement = {}; + } + + return placement; +} + +void +apply_include_placement_to_scene( + Eu7Scene &scene, + Eu7IncludePlacement const &binding, + std::vector const ¶meters ) { + auto const placement { + placement_transform_from_include_parameters( binding, parameters ) }; + if( false == transform_is_empty( placement ) ) { + compose_scene_with_include_prefix( scene, placement ); + } +} + +void +apply_include_placement_to_models( + std::vector &models, + Eu7IncludePlacement const &binding, + std::vector const ¶meters ) { + auto const placement { + placement_transform_from_include_parameters( binding, parameters ) }; + if( false == transform_is_empty( placement ) ) { + compose_models_with_prefix( models, placement ); + } +} + +std::string +module_load_key( + std::string const &resolved_path, + std::vector const ¶meters ) { + std::string key { resolved_path }; + for( auto const ¶meter : parameters ) { + key.push_back( '\0' ); + key += parameter; + } + return key; +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_parameters.h b/scene/eu7/eu7_parameters.h new file mode 100644 index 00000000..16552169 --- /dev/null +++ b/scene/eu7/eu7_parameters.h @@ -0,0 +1,51 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include +#include + +namespace scene::eu7 { + +void +bind_include_parameter( std::string &text, std::vector const ¶meters, bool to_lower = false ); + +void +apply_include_parameters_to_scene( Eu7Scene &scene, std::vector const ¶meters ); + +void +apply_include_parameters_to_models( std::vector &models, std::vector const ¶meters ); + +// Placement z chunku PLAC (indeksy pN z origin/rotate w .inc) + wartosci z INCL. +[[nodiscard]] Eu7TransformContext +placement_transform_from_include_parameters( + Eu7IncludePlacement const &binding, + std::vector const ¶meters ); + +void +apply_include_placement_to_scene( + Eu7Scene &scene, + Eu7IncludePlacement const &binding, + std::vector const ¶meters ); + +void +apply_include_placement_to_models( + std::vector &models, + Eu7IncludePlacement const &binding, + std::vector const ¶meters ); + +[[nodiscard]] std::string +module_load_key( + std::string const &resolved_path, + std::vector const ¶meters ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_reader.cpp b/scene/eu7/eu7_reader.cpp new file mode 100644 index 00000000..35d93320 --- /dev/null +++ b/scene/eu7/eu7_reader.cpp @@ -0,0 +1,1149 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_reader.h" + +#include "scene/eu7/eu7_chunks.h" +#include "scene/sn_utils.h" +#include "utilities/utilities.h" + +#include +#include +#include + +namespace scene::eu7 { +namespace { + +constexpr std::uint8_t kNodeFlagHasName { 1u << 0 }; +constexpr std::uint8_t kNodeFlagHasRangeMin { 1u << 1 }; +constexpr std::uint8_t kNodeFlagHasRangeMax { 1u << 2 }; +constexpr std::uint8_t kNodeFlagHasBounds { 1u << 3 }; +constexpr std::uint8_t kNodeFlagHasGroup { 1u << 4 }; +constexpr std::uint8_t kNodeFlagHasTransform { 1u << 5 }; +constexpr std::uint8_t kNodeFlagNotVisible { 1u << 6 }; + +constexpr std::uint8_t kTerrFlagTranslucent { 1u << 0 }; +constexpr std::uint8_t kTerrFlagNonDefaultLighting { 1u << 1 }; +constexpr std::uint8_t kTerrFlagBatched { 1u << 2 }; +constexpr std::size_t kTerrVertsPerRecord { 3 }; + +constexpr std::uint8_t kTrackTailCustom { 255 }; +constexpr std::uint32_t kMaxTrackPaths { 65536 }; +constexpr std::uint32_t kMaxTrackTailKeywords { 256 }; + +class StringTable { +public: + static constexpr std::uint32_t kEmpty { 0xFFFFFFFFu }; + + StringTable() = default; + + void + load( std::vector strings ) { + strings_ = std::move( strings ); + } + + explicit StringTable( std::vector const &strings ) + : strings_ { strings } {} + + [[nodiscard]] std::string const & + resolve( std::uint32_t const index ) const { + static std::string const empty; + if( index == kEmpty || index >= strings_.size() ) { + return empty; + } + return strings_[ index ]; + } + + [[nodiscard]] std::vector const & + strings() const noexcept { + return strings_; + } + +private: + std::vector strings_; +}; + +[[nodiscard]] std::int16_t +read_i16( std::istream &input ) { + return static_cast( sn_utils::ld_uint16( input ) ); +} + +[[nodiscard]] float +half_to_float( std::uint16_t const value ) { + const std::uint32_t sign { static_cast( 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; +} + +[[nodiscard]] float +snorm16_to_float( std::int16_t const value ) { + return static_cast( value ) * ( 1.f / 32767.f ); +} + +// EU7B v4/v5: io::writeF64/writeVec3 zapisuja float32 na dysku. +[[nodiscard]] double +read_f64_disk( std::istream &input ) { + return static_cast( sn_utils::ld_float32( input ) ); +} + +[[nodiscard]] glm::dvec3 +read_vec3( std::istream &input ) { + return { + static_cast( sn_utils::ld_float32( input ) ), + static_cast( sn_utils::ld_float32( input ) ), + static_cast( sn_utils::ld_float32( input ) ) }; +} + +[[nodiscard]] Eu7WorldVertex +read_packed_vertex( std::istream &input ) { + Eu7WorldVertex vertex; + vertex.position = read_vec3( input ); + vertex.normal.x = snorm16_to_float( read_i16( input ) ); + vertex.normal.y = snorm16_to_float( read_i16( input ) ); + vertex.normal.z = snorm16_to_float( read_i16( input ) ); + vertex.u = half_to_float( sn_utils::ld_uint16( input ) ); + vertex.v = half_to_float( sn_utils::ld_uint16( input ) ); + return vertex; +} + +[[nodiscard]] Eu7LightingData +read_lighting_block( std::istream &input ) { + Eu7LightingData lighting; + lighting.diffuse.x = sn_utils::ld_float32( input ); + lighting.diffuse.y = sn_utils::ld_float32( input ); + lighting.diffuse.z = sn_utils::ld_float32( input ); + lighting.diffuse.w = sn_utils::ld_float32( input ); + lighting.ambient.x = sn_utils::ld_float32( input ); + lighting.ambient.y = sn_utils::ld_float32( input ); + lighting.ambient.z = sn_utils::ld_float32( input ); + lighting.ambient.w = sn_utils::ld_float32( input ); + lighting.specular.x = sn_utils::ld_float32( input ); + lighting.specular.y = sn_utils::ld_float32( input ); + lighting.specular.z = sn_utils::ld_float32( input ); + lighting.specular.w = sn_utils::ld_float32( input ); + return lighting; +} + +[[nodiscard]] Eu7TransformContext +read_transform_context( std::istream &input ) { + Eu7TransformContext transform; + const std::uint8_t origin_count { sn_utils::d_uint8( input ) }; + transform.origin_stack.reserve( origin_count ); + for( std::uint8_t i { 0 }; i < origin_count; ++i ) { + transform.origin_stack.push_back( read_vec3( input ) ); + } + const std::uint8_t scale_count { sn_utils::d_uint8( input ) }; + transform.scale_stack.reserve( scale_count ); + for( std::uint8_t i { 0 }; i < scale_count; ++i ) { + transform.scale_stack.push_back( read_vec3( input ) ); + } + transform.rotation = read_vec3( input ); + transform.group_depth = sn_utils::d_uint8( input ); + return transform; +} + +[[nodiscard]] Eu7BasicNode +read_slim_node( std::istream &input, StringTable const &table, std::string_view const implied_type ) { + Eu7BasicNode node; + node.node_type = std::string( implied_type ); + const std::uint8_t flags { sn_utils::d_uint8( input ) }; + if( ( flags & kNodeFlagHasName ) != 0 ) { + node.name = table.resolve( sn_utils::ld_uint32( input ) ); + } + if( ( flags & kNodeFlagHasRangeMin ) != 0 ) { + node.range_squared_min = read_f64_disk( input ); + } + if( ( flags & kNodeFlagHasRangeMax ) != 0 ) { + node.range_squared_max = read_f64_disk( input ); + } + else { + node.range_squared_max = std::numeric_limits::max(); + } + if( ( flags & kNodeFlagHasBounds ) != 0 ) { + node.area.center = read_vec3( input ); + node.area.radius = sn_utils::ld_float32( input ); + } + if( ( flags & kNodeFlagHasGroup ) != 0 ) { + node.group_valid = true; + node.group_handle = sn_utils::ld_uint32( input ); + } + if( ( flags & kNodeFlagHasTransform ) != 0 ) { + node.transform = read_transform_context( input ); + } + node.visible = ( flags & kNodeFlagNotVisible ) == 0; + return node; +} + +[[nodiscard]] std::string +read_length_string( std::istream &input ) { + const std::uint32_t length { sn_utils::ld_uint32( input ) }; + std::string text( length, '\0' ); + if( length > 0 ) { + input.read( text.data(), static_cast( length ) ); + } + return text; +} + +void +read_strs_chunk( std::istream &input, StringTable &table ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + std::vector strings; + strings.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + strings.push_back( read_length_string( input ) ); + } + table.load( std::move( strings ) ); +} + +[[nodiscard]] std::string_view +mesh_subtype_name( std::uint8_t const code ) noexcept { + switch( code ) { + case 1: + return "triangle_strip"; + case 2: + return "triangle_fan"; + default: + return "triangles"; + } +} + +[[nodiscard]] std::string_view +line_subtype_name( std::uint8_t const code ) noexcept { + switch( code ) { + case 1: + return "line_strip"; + case 2: + return "line_loop"; + default: + return "lines"; + } +} + +[[nodiscard]] std::string +track_tail_keyword_name( std::uint8_t const 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 {}; + } +} + +[[nodiscard]] std::pair +read_track_tail_entry( std::istream &input, StringTable const &table ) { + const std::uint8_t code { sn_utils::d_uint8( input ) }; + std::string key; + if( code == kTrackTailCustom ) { + key = table.resolve( sn_utils::ld_uint32( input ) ); + } + else { + key = track_tail_keyword_name( code ); + } + return { std::move( key ), table.resolve( sn_utils::ld_uint32( input ) ) }; +} + +[[nodiscard]] Eu7SegmentPath +read_segment_path( std::istream &input ) { + Eu7SegmentPath seg; + seg.p_start = read_vec3( input ); + seg.roll_start = read_f64_disk( input ); + seg.cp_out = read_vec3( input ); + seg.cp_in = read_vec3( input ); + seg.p_end = read_vec3( input ); + seg.roll_end = read_f64_disk( input ); + seg.radius = read_f64_disk( input ); + return seg; +} + +[[nodiscard]] Eu7TrackVisibility +read_track_visibility( std::istream &input, StringTable const &table ) { + Eu7TrackVisibility vis; + vis.material1 = table.resolve( sn_utils::ld_uint32( input ) ); + vis.tex_length = sn_utils::ld_float32( input ); + vis.material2 = table.resolve( sn_utils::ld_uint32( input ) ); + vis.tex_height1 = sn_utils::ld_float32( input ); + vis.tex_width = sn_utils::ld_float32( input ); + vis.tex_slope = sn_utils::ld_float32( input ); + return vis; +} + +[[nodiscard]] Eu7Track +read_runtime_track( std::istream &input, StringTable const &table ) { + Eu7Track track; + track.node = read_slim_node( input, table, "track" ); + track.track_type = static_cast( sn_utils::d_uint8( input ) ); + track.category = static_cast( sn_utils::d_uint8( input ) ); + track.length = sn_utils::ld_float32( input ); + track.track_width = sn_utils::ld_float32( input ); + track.friction = sn_utils::ld_float32( input ); + track.sound_distance = sn_utils::ld_float32( input ); + track.quality_flag = sn_utils::ld_int32( input ); + track.damage_flag = sn_utils::ld_int32( input ); + track.environment = static_cast( static_cast( sn_utils::d_uint8( input ) ) - 1 ); + if( sn_utils::d_uint8( input ) != 0 ) { + track.visibility = read_track_visibility( input, table ); + } + const std::uint32_t path_count { sn_utils::ld_uint32( input ) }; + if( path_count > kMaxTrackPaths ) { + throw std::runtime_error( + "EU7B TRAK: path_count=" + std::to_string( path_count ) + " (uszkodzony tor \"" + + track.node.name + "\")" ); + } + track.paths.reserve( path_count ); + for( std::uint32_t i { 0 }; i < path_count; ++i ) { + track.paths.push_back( read_segment_path( input ) ); + } + const std::uint32_t tail_count { sn_utils::ld_uint32( input ) }; + if( tail_count > kMaxTrackTailKeywords ) { + throw std::runtime_error( + "EU7B TRAK: tail_count=" + std::to_string( tail_count ) + " (uszkodzony tor \"" + + track.node.name + "\")" ); + } + track.tail_keywords.reserve( tail_count ); + for( std::uint32_t i { 0 }; i < tail_count; ++i ) { + track.tail_keywords.push_back( read_track_tail_entry( input, table ) ); + } + return track; +} + +[[nodiscard]] Eu7Shape +read_runtime_shape( std::istream &input, StringTable const &table ) { + Eu7Shape shape; + const std::uint8_t subtype { sn_utils::d_uint8( input ) }; + shape.node = read_slim_node( input, table, mesh_subtype_name( subtype ) ); + shape.translucent = sn_utils::d_uint8( input ) != 0; + shape.material_path = table.resolve( sn_utils::ld_uint32( input ) ); + if( sn_utils::d_uint8( input ) != 0 ) { + shape.lighting = read_lighting_block( input ); + } + shape.origin = read_vec3( input ); + const std::uint32_t vertex_count { sn_utils::ld_uint32( input ) }; + shape.vertices.reserve( vertex_count ); + for( std::uint32_t i { 0 }; i < vertex_count; ++i ) { + shape.vertices.push_back( read_packed_vertex( input ) ); + } + return shape; +} + +[[nodiscard]] Eu7Lines +read_runtime_lines( std::istream &input, StringTable const &table ) { + Eu7Lines lines; + const std::uint8_t subtype { sn_utils::d_uint8( input ) }; + lines.node = read_slim_node( input, table, line_subtype_name( subtype ) ); + if( sn_utils::d_uint8( input ) != 0 ) { + lines.lighting = read_lighting_block( input ); + } + lines.line_width = sn_utils::ld_float32( input ); + lines.origin = read_vec3( input ); + const std::uint32_t vertex_count { sn_utils::ld_uint32( input ) }; + lines.vertices.reserve( vertex_count ); + for( std::uint32_t i { 0 }; i < vertex_count; ++i ) { + Eu7WorldVertex vertex; + vertex.position = read_vec3( input ); + lines.vertices.push_back( vertex ); + } + return lines; +} + +[[nodiscard]] Eu7Model +read_runtime_model( std::istream &input, StringTable const &table ) { + Eu7Model model; + model.node = read_slim_node( input, table, "model" ); + model.is_terrain = sn_utils::d_uint8( input ) != 0; + model.transition = sn_utils::d_uint8( input ) != 0; + model.location = read_vec3( input ); + model.angles = read_vec3( input ); + model.scale = read_vec3( input ); + model.model_file = table.resolve( sn_utils::ld_uint32( input ) ); + model.texture_file = table.resolve( sn_utils::ld_uint32( input ) ); + const std::uint32_t light_count { sn_utils::ld_uint32( input ) }; + model.light_states.resize( light_count ); + for( std::uint32_t i { 0 }; i < light_count; ++i ) { + model.light_states[ i ] = sn_utils::ld_float32( input ); + } + const std::uint32_t color_count { sn_utils::ld_uint32( input ) }; + model.light_colors.resize( color_count ); + for( std::uint32_t i { 0 }; i < color_count; ++i ) { + model.light_colors[ i ] = sn_utils::ld_uint32( input ); + } + return model; +} + +[[nodiscard]] Eu7ModelPrototype +read_runtime_prototype( std::istream &input, StringTable const &table ) { + Eu7ModelPrototype proto; + proto.node = read_slim_node( input, table, "model" ); + proto.is_terrain = sn_utils::d_uint8( input ) != 0; + proto.transition = sn_utils::d_uint8( input ) != 0; + proto.model_file = table.resolve( sn_utils::ld_uint32( input ) ); + proto.texture_file = table.resolve( sn_utils::ld_uint32( input ) ); + const std::uint32_t light_count { sn_utils::ld_uint32( input ) }; + proto.light_states.resize( light_count ); + for( std::uint32_t i { 0 }; i < light_count; ++i ) { + proto.light_states[ i ] = sn_utils::ld_float32( input ); + } + const std::uint32_t color_count { sn_utils::ld_uint32( input ) }; + proto.light_colors.resize( color_count ); + for( std::uint32_t i { 0 }; i < color_count; ++i ) { + proto.light_colors[ i ] = sn_utils::ld_uint32( input ); + } + return proto; +} + +void +read_prot_chunk( std::istream &input, StringTable const &strings, Eu7Module &module ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.model_prototypes.clear(); + module.model_prototypes.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.model_prototypes.push_back( read_runtime_prototype( input, strings ) ); + } +} + +[[nodiscard]] Eu7Model +expand_prototype_instance( + Eu7ModelPrototype const &proto, + glm::dvec3 const &location, + glm::dvec3 const &angles, + glm::dvec3 const &scale, + std::string const &name ) { + Eu7Model model; + model.node = proto.node; + model.node.name = name; + model.node.transform = {}; + model.location = location; + model.angles = angles; + model.scale = scale; + model.model_file = proto.model_file; + model.texture_file = proto.texture_file; + model.light_states = proto.light_states; + model.light_colors = proto.light_colors; + model.transition = proto.transition; + model.is_terrain = proto.is_terrain; + return model; +} + +void +parse_pack_section_header( + Eu7Module const &module, + std::istream &input, + Eu7PackIndexEntry const &entry, + Eu7PackSectionCursor &cursor ) { + cursor = {}; + input.seekg( + static_cast( module.pack_payload_offset + entry.pack_offset ) ); + + const auto peek { input.peek() }; + const std::uint8_t first_byte { + peek == EOF ? std::uint8_t { 0 } : static_cast( peek ) }; + + if( first_byte == kPackSectionFormatV8 ) { + cursor.section_format = sn_utils::d_uint8( input ); + const std::uint32_t solo_total { sn_utils::ld_uint32( input ) }; + const std::uint32_t inst_total { sn_utils::ld_uint32( input ) }; + cursor.model_total = solo_total + inst_total; + cursor.solo_remaining = solo_total; + cursor.inst_remaining = inst_total; + } + else if( first_byte == 2 ) { + throw std::runtime_error( + "EU7 PACK: format v9 nieobslugiwany — przebake plik .eu7" ); + } + else { + cursor.model_total = entry.model_count; + cursor.solo_remaining = entry.model_count; + cursor.inst_remaining = 0; + } + + cursor.models_read = 0; + cursor.header_parsed = true; +} + +[[nodiscard]] std::vector +read_pack_models_chunk_impl( + Eu7Module const &module, + std::istream &input, + Eu7PackSectionCursor &cursor, + std::size_t const max_count, + StringTable const &strings ) { + if( false == cursor.header_parsed || max_count == 0 ) { + return {}; + } + + std::vector models; + auto const remaining { + static_cast( cursor.solo_remaining ) + + static_cast( cursor.inst_remaining ) }; + models.reserve( std::min( max_count, remaining ) ); + + while( + models.size() < max_count && + ( cursor.solo_remaining > 0 || cursor.inst_remaining > 0 ) ) { + if( cursor.solo_remaining > 0 ) { + auto model { read_runtime_model( input, strings ) }; + model.node.transform = {}; + models.push_back( std::move( model ) ); + --cursor.solo_remaining; + } + else { + const std::uint32_t proto_id { sn_utils::ld_uint32( input ) }; + if( proto_id >= module.model_prototypes.size() ) { + throw std::runtime_error( + "EU7 PACK v8: proto_id " + std::to_string( proto_id ) + " poza zakresem PROT (" + + std::to_string( module.model_prototypes.size() ) + ")" ); + } + auto const location { read_vec3( input ) }; + auto const angles { read_vec3( input ) }; + auto const scale { read_vec3( input ) }; + auto const name { strings.resolve( sn_utils::ld_uint32( input ) ) }; + models.push_back( expand_prototype_instance( + module.model_prototypes[ proto_id ], + location, + angles, + scale, + name ) ); + --cursor.inst_remaining; + } + ++cursor.models_read; + } + + return models; +} + +[[nodiscard]] Eu7Traction +read_runtime_traction( std::istream &input, StringTable const &table ) { + Eu7Traction traction; + traction.node = read_slim_node( input, table, "traction" ); + traction.power_supply_name = table.resolve( sn_utils::ld_uint32( input ) ); + traction.material = static_cast( sn_utils::d_uint8( input ) ); + traction.nominal_voltage = sn_utils::ld_float32( input ); + traction.max_current = sn_utils::ld_float32( input ); + traction.resistivity_ohm_per_m = sn_utils::ld_float32( input ); + traction.resistivity_legacy = read_f64_disk( input ); + traction.material_raw = table.resolve( sn_utils::ld_uint32( input ) ); + traction.wire_thickness = sn_utils::ld_float32( input ); + traction.damage_flag = sn_utils::ld_int32( input ); + traction.wire_p1 = read_vec3( input ); + traction.wire_p2 = read_vec3( input ); + traction.wire_p3 = read_vec3( input ); + traction.wire_p4 = read_vec3( input ); + traction.min_height = read_f64_disk( input ); + traction.segment_length = read_f64_disk( input ); + traction.wire_count = sn_utils::ld_int32( input ); + traction.wire_offset = sn_utils::ld_float32( input ); + if( sn_utils::d_uint8( input ) != 0 ) { + auto const parallel { table.resolve( sn_utils::ld_uint32( input ) ) }; + if( !parallel.empty() ) { + traction.parallel_name = parallel; + } + } + return traction; +} + +[[nodiscard]] Eu7TractionPowerSource +read_runtime_power_source( std::istream &input, StringTable const &table ) { + Eu7TractionPowerSource source; + source.node = read_slim_node( input, table, "tractionpowersource" ); + source.position = read_vec3( input ); + source.node.area.center = source.position; + source.nominal_voltage = sn_utils::ld_float32( input ); + source.voltage_frequency = sn_utils::ld_float32( input ); + source.internal_resistance_legacy = read_f64_disk( input ); + source.internal_resistance = sn_utils::ld_float32( input ); + source.max_output_current = sn_utils::ld_float32( input ); + source.fast_fuse_timeout = sn_utils::ld_float32( input ); + source.fast_fuse_repetition = sn_utils::ld_float32( input ); + source.slow_fuse_timeout = sn_utils::ld_float32( input ); + source.modifier = static_cast( sn_utils::d_uint8( input ) ); + return source; +} + +[[nodiscard]] Eu7MemCell +read_runtime_memcell( std::istream &input, StringTable const &table ) { + Eu7MemCell cell; + cell.node = read_slim_node( input, table, "memcell" ); + cell.text = table.resolve( sn_utils::ld_uint32( input ) ); + cell.value1 = read_f64_disk( input ); + cell.value2 = read_f64_disk( input ); + auto const track { table.resolve( sn_utils::ld_uint32( input ) ) }; + if( !track.empty() ) { + cell.track_name = track; + } + return cell; +} + +[[nodiscard]] Eu7EventLauncher +read_runtime_launcher( std::istream &input, StringTable const &table ) { + Eu7EventLauncher launcher; + launcher.node = read_slim_node( input, table, "eventlauncher" ); + launcher.location = read_vec3( input ); + launcher.radius_squared = read_f64_disk( input ); + launcher.activation_key_raw = table.resolve( sn_utils::ld_uint32( input ) ); + launcher.activation_key = sn_utils::ld_int32( input ); + launcher.delta_time = read_f64_disk( input ); + launcher.event1_name = table.resolve( sn_utils::ld_uint32( input ) ); + launcher.event2_name = table.resolve( sn_utils::ld_uint32( input ) ); + launcher.launch_hour = sn_utils::ld_int32( input ); + launcher.launch_minute = sn_utils::ld_int32( input ); + if( sn_utils::d_uint8( input ) != 0 ) { + Eu7EventLauncherCondition cond; + cond.memcell_name = table.resolve( sn_utils::ld_uint32( input ) ); + cond.compare_text = table.resolve( sn_utils::ld_uint32( input ) ); + cond.compare_value1 = read_f64_disk( input ); + cond.compare_value2 = read_f64_disk( input ); + cond.check_mask = sn_utils::ld_int32( input ); + launcher.condition = cond; + } + launcher.train_triggered = sn_utils::d_uint8( input ) != 0; + return launcher; +} + +[[nodiscard]] Eu7Dynamic +read_runtime_dynamic( std::istream &input, StringTable const &table ) { + Eu7Dynamic vehicle; + vehicle.node = read_slim_node( input, table, "dynamic" ); + vehicle.data_folder = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.skin_file = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.mmd_file = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.track_name = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.driver_type = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.load_type = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.coupling_params = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.coupling_raw = table.resolve( sn_utils::ld_uint32( input ) ); + vehicle.offset = read_f64_disk( input ); + vehicle.coupling = sn_utils::ld_int32( input ); + if( vehicle.coupling_raw.empty() ) { + if( !vehicle.coupling_params.empty() ) { + vehicle.coupling_raw = std::to_string( vehicle.coupling ) + "." + vehicle.coupling_params; + } + else { + vehicle.coupling_raw = std::to_string( vehicle.coupling ); + } + } + vehicle.load_count = sn_utils::ld_int32( input ); + vehicle.velocity = sn_utils::ld_float32( input ); + if( sn_utils::d_uint8( input ) != 0 ) { + vehicle.destination = table.resolve( sn_utils::ld_uint32( input ) ); + } + if( sn_utils::d_uint8( input ) != 0 ) { + vehicle.trainset_index = sn_utils::ld_uint32( input ); + } + return vehicle; +} + +[[nodiscard]] Eu7Sound +read_runtime_sound( std::istream &input, StringTable const &table ) { + Eu7Sound sound; + sound.node = read_slim_node( input, table, "sound" ); + sound.location = read_vec3( input ); + sound.wav_file = table.resolve( sn_utils::ld_uint32( input ) ); + return sound; +} + +[[nodiscard]] Eu7Trainset +read_runtime_trainset( std::istream &input, StringTable const &table ) { + Eu7Trainset trainset; + trainset.name = table.resolve( sn_utils::ld_uint32( input ) ); + trainset.track = table.resolve( sn_utils::ld_uint32( input ) ); + trainset.offset = sn_utils::ld_float32( input ); + trainset.velocity = sn_utils::ld_float32( input ); + const std::uint32_t assignment_count { sn_utils::ld_uint32( input ) }; + for( std::uint32_t i { 0 }; i < assignment_count; ++i ) { + auto const key { table.resolve( sn_utils::ld_uint32( input ) ) }; + auto const value { table.resolve( sn_utils::ld_uint32( input ) ) }; + trainset.assignment.emplace( key, value ); + } + const std::uint32_t vehicle_count { sn_utils::ld_uint32( input ) }; + trainset.vehicle_indices.reserve( vehicle_count ); + for( std::uint32_t i { 0 }; i < vehicle_count; ++i ) { + trainset.vehicle_indices.push_back( sn_utils::ld_uint32( input ) ); + } + const std::uint32_t coupling_count { sn_utils::ld_uint32( input ) }; + trainset.couplings.reserve( coupling_count ); + for( std::uint32_t i { 0 }; i < coupling_count; ++i ) { + trainset.couplings.push_back( sn_utils::ld_int32( input ) ); + } + trainset.driver_index = sn_utils::ld_uint32( input ); + return trainset; +} + +[[nodiscard]] Eu7Event +read_runtime_event( std::istream &input, StringTable const &table ) { + Eu7Event event; + event.name = table.resolve( sn_utils::ld_uint32( input ) ); + event.type = static_cast( sn_utils::d_uint8( input ) ); + event.delay = read_f64_disk( input ); + const std::uint32_t target_count { sn_utils::ld_uint32( input ) }; + event.targets.reserve( target_count ); + for( std::uint32_t i { 0 }; i < target_count; ++i ) { + event.targets.push_back( table.resolve( sn_utils::ld_uint32( input ) ) ); + } + event.delay_random = read_f64_disk( input ); + event.delay_departure = read_f64_disk( input ); + event.ignored = sn_utils::d_uint8( input ) != 0; + event.passive = sn_utils::d_uint8( input ) != 0; + const std::uint32_t payload_count { sn_utils::ld_uint32( input ) }; + event.payload.reserve( payload_count ); + for( std::uint32_t i { 0 }; i < payload_count; ++i ) { + event.payload.emplace_back( + table.resolve( sn_utils::ld_uint32( input ) ), + table.resolve( sn_utils::ld_uint32( input ) ) ); + } + return event; +} + +[[nodiscard]] Eu7Shape +make_terr_shape( std::string const &material, std::uint8_t const flags, Eu7LightingData const &lighting ) { + Eu7Shape shape; + shape.node.node_type = "triangles"; + shape.node.range_squared_min = 0.0; + shape.node.range_squared_max = std::numeric_limits::max(); + shape.node.visible = true; + shape.material_path = material; + shape.translucent = ( flags & kTerrFlagTranslucent ) != 0; + shape.lighting = lighting; + return shape; +} + +void +read_terr_chunk( std::istream &input, StringTable const &table, Eu7Scene &scene ) { + const std::uint8_t flags { sn_utils::d_uint8( input ) }; + const std::string material { table.resolve( sn_utils::ld_uint32( input ) ) }; + + Eu7LightingData lighting; + if( ( flags & kTerrFlagNonDefaultLighting ) != 0 ) { + lighting = read_lighting_block( input ); + } + + if( ( flags & kTerrFlagBatched ) != 0 ) { + const std::uint32_t batch_count { sn_utils::ld_uint32( input ) }; + scene.terrain_shapes.reserve( scene.terrain_shapes.size() + batch_count ); + for( std::uint32_t i { 0 }; i < batch_count; ++i ) { + (void)sn_utils::ld_int32( input ); + (void)sn_utils::ld_int32( input ); + Eu7Shape shape { make_terr_shape( material, flags, lighting ) }; + const std::uint32_t vertex_count { sn_utils::ld_uint32( input ) }; + if( vertex_count % kTerrVertsPerRecord != 0 ) { + throw std::runtime_error( "EU7B TERR: liczba wierzcholkow musi byc wielokrotnoscia 3" ); + } + shape.vertices.resize( vertex_count ); + for( std::uint32_t v { 0 }; v < vertex_count; ++v ) { + shape.vertices[ v ] = read_packed_vertex( input ); + } + scene.terrain_shapes.push_back( std::move( shape ) ); + } + return; + } + + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + scene.terrain_shapes.reserve( scene.terrain_shapes.size() + count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + Eu7Shape shape { make_terr_shape( material, flags, lighting ) }; + shape.vertices.resize( kTerrVertsPerRecord ); + for( std::size_t v { 0 }; v < kTerrVertsPerRecord; ++v ) { + shape.vertices[ v ] = read_packed_vertex( input ); + } + scene.terrain_shapes.push_back( std::move( shape ) ); + } +} + +void +build_pack_section_index( Eu7PackCatalog &catalog ) { + catalog.index_by_section.clear(); + catalog.index_by_section.reserve( catalog.entries.size() ); + for( std::size_t i { 0 }; i < catalog.entries.size(); ++i ) { + auto const &entry { catalog.entries[ i ] }; + auto const key { + ( static_cast( entry.row ) << 16 ) | + static_cast( entry.column ) }; + catalog.index_by_section.emplace( key, i ); + } +} + +void +read_chunk_payload( + std::uint32_t const chunk_type, + std::istream &input, + StringTable &strings, + Eu7Module &module, + std::uint32_t const file_version ) { + if( chunk_type == kChunkStrs ) { + read_strs_chunk( input, strings ); + } + else if( chunk_type == kChunkIncl ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.includes.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + Eu7Include inc; + inc.source_line = sn_utils::ld_uint32( input ); + inc.source_path = strings.resolve( sn_utils::ld_uint32( input ) ); + inc.binary_path = strings.resolve( sn_utils::ld_uint32( input ) ); + const std::uint32_t param_count { sn_utils::ld_uint32( input ) }; + inc.parameters.reserve( param_count ); + for( std::uint32_t p { 0 }; p < param_count; ++p ) { + inc.parameters.push_back( strings.resolve( sn_utils::ld_uint32( input ) ) ); + } + if( file_version >= kEu7VersionV6 ) { + inc.site_transform = read_transform_context( input ); + } + module.includes.push_back( std::move( inc ) ); + } + } + else if( chunk_type == kChunkTrak ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.tracks.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.tracks.push_back( read_runtime_track( input, strings ) ); + } + } + else if( chunk_type == kChunkTerr ) { + module.has_terrain_chunk = true; + read_terr_chunk( input, strings, module.scene ); + } + else if( chunk_type == kChunkMesh ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.shapes.reserve( module.scene.shapes.size() + count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.shapes.push_back( read_runtime_shape( input, strings ) ); + } + } + else if( chunk_type == kChunkLine ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.lines.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.lines.push_back( read_runtime_lines( input, strings ) ); + } + } + else if( chunk_type == kChunkModl ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.models.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.models.push_back( read_runtime_model( input, strings ) ); + } + } + else if( chunk_type == kChunkTrac ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.traction.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.traction.push_back( read_runtime_traction( input, strings ) ); + } + } + else if( chunk_type == kChunkPwrs ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.power_sources.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.power_sources.push_back( read_runtime_power_source( input, strings ) ); + } + } + else if( chunk_type == kChunkMemc ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.memcells.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.memcells.push_back( read_runtime_memcell( input, strings ) ); + } + } + else if( chunk_type == kChunkLaun ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.event_launchers.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.event_launchers.push_back( read_runtime_launcher( input, strings ) ); + } + } + else if( chunk_type == kChunkDynm ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.dynamics.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.dynamics.push_back( read_runtime_dynamic( input, strings ) ); + } + } + else if( chunk_type == kChunkSond ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.sounds.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.sounds.push_back( read_runtime_sound( input, strings ) ); + } + } + else if( chunk_type == kChunkTrset ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.trainsets.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.trainsets.push_back( read_runtime_trainset( input, strings ) ); + } + } + else if( chunk_type == kChunkEvnt ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.scene.events.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + module.scene.events.push_back( read_runtime_event( input, strings ) ); + } + } + else if( chunk_type == kChunkFint ) { + module.scene.first_init_count = sn_utils::ld_uint32( input ); + } + else if( chunk_type == kChunkPlac ) { + module.include_placement.origin_x_param = sn_utils::d_uint8( input ); + module.include_placement.origin_y_param = sn_utils::d_uint8( input ); + module.include_placement.origin_z_param = sn_utils::d_uint8( input ); + module.include_placement.rotation_y_param = sn_utils::d_uint8( input ); + } + else if( chunk_type == kChunkPidx ) { + const std::uint32_t count { sn_utils::ld_uint32( input ) }; + module.pack_catalog.entries.clear(); + module.pack_catalog.entries.reserve( count ); + for( std::uint32_t i { 0 }; i < count; ++i ) { + Eu7PackIndexEntry entry; + entry.row = sn_utils::ld_uint16( input ); + entry.column = sn_utils::ld_uint16( input ); + entry.model_count = sn_utils::ld_uint32( input ); + entry.pack_offset = sn_utils::ld_uint64( input ); + module.pack_catalog.entries.push_back( entry ); + } + build_pack_section_index( module.pack_catalog ); + } + else if( chunk_type == kChunkProt ) { + read_prot_chunk( input, strings, module ); + } +} + +[[nodiscard]] std::optional +find_pack_entry_impl( Eu7Module const &module, int const row, int const column ) { + if( row < 0 || row > 0xFFFF || column < 0 || column > 0xFFFF ) { + return std::nullopt; + } + + if( false == module.pack_catalog.index_by_section.empty() ) { + auto const key { + ( static_cast( row ) << 16 ) | + static_cast( column ) }; + auto const found { module.pack_catalog.index_by_section.find( key ) }; + if( found == module.pack_catalog.index_by_section.end() ) { + return std::nullopt; + } + return module.pack_catalog.entries[ found->second ]; + } + + for( auto const &entry : module.pack_catalog.entries ) { + if( entry.row == static_cast( row ) && + entry.column == static_cast( column ) ) { + return entry; + } + } + return std::nullopt; +} + +[[nodiscard]] std::vector +read_pack_section_impl( Eu7Module const &module, int const row, int const column ) { + if( false == module.has_pack_chunk || module.source_path.empty() ) { + return {}; + } + + auto const entry { find_pack_entry_impl( module, row, column ) }; + if( false == entry.has_value() || entry->model_count == 0 ) { + return {}; + } + + std::ifstream input { module.source_path, std::ios::binary }; + if( !input ) { + throw std::runtime_error( + "EU7 PACK: nie mozna otworzyc \"" + module.source_path + "\"" ); + } + + StringTable const strings { module.strings }; + Eu7PackSectionCursor header; + parse_pack_section_header( module, input, *entry, header ); + + return read_pack_models_chunk_impl( + module, input, header, std::numeric_limits::max(), strings ); +} + +} // namespace + +bool +is_valid_eu7b_file( std::string const &path ) { + if( false == FileExists( path ) ) { + return false; + } + std::ifstream input { path, std::ios::binary }; + if( !input ) { + return false; + } + if( sn_utils::ld_uint32( input ) != kEu7Magic ) { + return false; + } + const std::uint32_t version { sn_utils::ld_uint32( input ) }; + return ( + version == kEu7VersionV4 || version == kEu7VersionV5 || version == kEu7VersionV6 || + version == kEu7VersionV7 || version == kEu7VersionV8 ); +} + +Eu7Module +read_module( std::string const &path ) { + std::ifstream input { path, std::ios::binary }; + if( !input ) { + throw std::runtime_error( "EU7: nie mozna otworzyc \"" + path + "\"" ); + } + + if( sn_utils::ld_uint32( input ) != kEu7Magic ) { + throw std::runtime_error( "EU7B: zly magic w \"" + path + "\"" ); + } + + Eu7Module module; + module.version = sn_utils::ld_uint32( input ); + if( + module.version != kEu7VersionV4 && module.version != kEu7VersionV5 && + module.version != kEu7VersionV6 && module.version != kEu7VersionV7 && + module.version != kEu7VersionV8 ) { + throw std::runtime_error( + "EU7B: nieobslugiwana wersja " + std::to_string( module.version ) + " w \"" + path + "\"" ); + } + + StringTable strings; + + while( input.peek() != EOF ) { + const std::uint32_t chunk_type { sn_utils::ld_uint32( input ) }; + const std::uint32_t chunk_size { sn_utils::ld_uint32( input ) }; + if( chunk_size < 8 ) { + throw std::runtime_error( "EU7B: uszkodzony chunk w \"" + path + "\"" ); + } + const auto chunk_start { input.tellg() }; + const std::streamoff payload_size { + static_cast( chunk_size ) - static_cast( 8 ) }; + + if( chunk_type == kChunkPack ) { + module.has_pack_chunk = true; + module.pack_catalog.pack_payload_size = static_cast( payload_size ); + module.pack_payload_offset = static_cast( chunk_start ); + input.seekg( chunk_start + payload_size ); + continue; + } + + read_chunk_payload( chunk_type, input, strings, module, module.version ); + + input.seekg( chunk_start + payload_size ); + } + + module.strings = strings.strings(); + module.source_path = path; + return module; +} + +std::optional +find_pack_entry( Eu7Module const &module, int const row, int const column ) { + return find_pack_entry_impl( module, row, column ); +} + +std::vector +read_pack_section( Eu7Module const &module, int const row, int const column ) { + return read_pack_section_impl( module, row, column ); +} + +void +seek_pack_section( + Eu7Module const &module, + std::istream &input, + Eu7PackIndexEntry const &entry, + Eu7PackSectionCursor &cursor ) { + if( false == module.has_pack_chunk ) { + cursor = {}; + return; + } + parse_pack_section_header( module, input, entry, cursor ); +} + +std::vector +read_pack_models_chunk( + Eu7Module const &module, + std::istream &input, + Eu7PackSectionCursor &cursor, + std::size_t const max_count ) { + StringTable const strings { module.strings }; + return read_pack_models_chunk_impl( module, input, cursor, max_count, strings ); +} + +void +resume_pack_section( + Eu7Module const &module, + std::istream &input, + Eu7PackIndexEntry const &entry, + std::uint64_t const resume_byte_offset, + Eu7PackSectionCursor const resume_cursor, + Eu7PackSectionCursor &cursor ) { + if( false == module.has_pack_chunk ) { + cursor = {}; + return; + } + + input.seekg( + static_cast( + module.pack_payload_offset + entry.pack_offset + resume_byte_offset ) ); + cursor = resume_cursor; +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_reader.h b/scene/eu7/eu7_reader.h new file mode 100644 index 00000000..0cdc4286 --- /dev/null +++ b/scene/eu7/eu7_reader.h @@ -0,0 +1,61 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include +#include +#include +#include + +namespace scene::eu7 { + +[[nodiscard]] Eu7Module +read_module( std::string const &Path ); + +[[nodiscard]] bool +is_valid_eu7b_file( std::string const &Path ); + +// Szuka wpisu PIDX dla sekcji 1 km (row/column jak basic_region::section). +[[nodiscard]] std::optional +find_pack_entry( Eu7Module const &Module, int Row, int Column ); + +// Odczyt MODL z chunku PACK dla jednej sekcji (world-space, po bake). +[[nodiscard]] std::vector +read_pack_section( Eu7Module const &Module, int Row, int Column ); + +// Seek do pack_offset z PIDX i parsuj naglowek sekcji (v7/v8). +void +seek_pack_section( + Eu7Module const &Module, + std::istream &Input, + Eu7PackIndexEntry const &Entry, + Eu7PackSectionCursor &Cursor ); + +// Odczyt do max_count modeli z biezacej pozycji strumienia (po seek_pack_section). +[[nodiscard]] std::vector +read_pack_models_chunk( + Eu7Module const &Module, + std::istream &Input, + Eu7PackSectionCursor &Cursor, + std::size_t MaxCount ); + +// Wznowienie odczytu sub-chunka (offset wzgledem pack_offset sekcji z PIDX). +void +resume_pack_section( + Eu7Module const &Module, + std::istream &Input, + Eu7PackIndexEntry const &Entry, + std::uint64_t ResumeByteOffset, + Eu7PackSectionCursor ResumeCursor, + Eu7PackSectionCursor &Cursor ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_section.cpp b/scene/eu7/eu7_section.cpp new file mode 100644 index 00000000..b3878165 --- /dev/null +++ b/scene/eu7/eu7_section.cpp @@ -0,0 +1,47 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_section.h" + +#include +#include + +namespace scene::eu7 { + +std::pair +section_row_column( glm::dvec3 const &world_location ) { + auto const column { + static_cast( std::floor( + world_location.x / static_cast( kSectionSizeM ) + + kRegionSideSectionCount / 2 ) ) }; + auto const row { + static_cast( std::floor( + world_location.z / static_cast( kSectionSizeM ) + + kRegionSideSectionCount / 2 ) ) }; + + return { + std::clamp( row, 0, kRegionSideSectionCount - 1 ), + std::clamp( column, 0, kRegionSideSectionCount - 1 ) }; +} + +std::size_t +section_index( glm::dvec3 const &world_location ) { + auto const [row, column] { section_row_column( world_location ) }; + return section_index( row, column ); +} + +std::size_t +section_index( int const row, int const column ) { + return ( + static_cast( row ) * static_cast( kRegionSideSectionCount ) + + static_cast( column ) ); +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_section.h b/scene/eu7/eu7_section.h new file mode 100644 index 00000000..3a56ee56 --- /dev/null +++ b/scene/eu7/eu7_section.h @@ -0,0 +1,33 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include + +namespace scene::eu7 { + +// Musi byc zgodne z scene::EU07_SECTIONSIZE / EU07_REGIONSIDESECTIONCOUNT. +constexpr int kSectionSizeM { 1000 }; +constexpr int kRegionSideSectionCount { 500 }; + +[[nodiscard]] std::pair +section_row_column( glm::dvec3 const &world_location ); + +[[nodiscard]] std::size_t +section_index( glm::dvec3 const &world_location ); + +[[nodiscard]] std::size_t +section_index( int const row, int const column ); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_section_stream.cpp b/scene/eu7/eu7_section_stream.cpp new file mode 100644 index 00000000..2eb5dd47 --- /dev/null +++ b/scene/eu7/eu7_section_stream.cpp @@ -0,0 +1,2042 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/eu7/eu7_section_stream.h" + +#include "scene/eu7/eu7_chunks.h" +#include "scene/eu7/eu7_load_stats.h" +#include "scene/eu7/eu7_pack_bench.h" +#include "model/AnimModel.h" +#include "model/MdlMngr.h" +#include "scene/eu7/eu7_model_prefetch.h" +#include "scene/eu7/eu7_reader.h" +#include "scene/eu7/eu7_section.h" +#include "simulation/simulation.h" +#include "simulation/simulationstateserializer.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/Timer.h" +#include "utilities/utilities.h" +#include "vehicle/DynObj.h" +#include "world/Track.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace scene::eu7 { +namespace { + +constexpr double kDrainBudgetMs { 12.0 }; +constexpr double kLoaderDrainBudgetMs { 48.0 }; +constexpr std::size_t kLoaderSectionsPerDrain { 8 }; + +constexpr double kGameplayApplyBudgetMs { 4.0 }; +constexpr double kCatchupApplyBudgetMs { 6.0 }; +constexpr std::size_t kGameplaySliceInstances { 96 }; +constexpr std::size_t kCatchupSliceInstances { 160 }; +constexpr std::size_t kLoaderSliceInstances { 768 }; +constexpr std::size_t kGameplaySliceColdMeshes { 1 }; +constexpr std::size_t kCatchupSliceColdMeshes { 2 }; +constexpr std::size_t kLoaderSliceColdMeshes { 32 }; +constexpr double kGameplayColdBudgetMs { 3.0 }; +constexpr double kCatchupColdBudgetMs { 5.0 }; + +constexpr int kInitialBootstrapRadius { 4 }; +constexpr int kStreamRadius { 11 }; +constexpr int kMovementLookahead { 7 }; +constexpr std::size_t kMaxPackStreamWorkers { 4 }; +constexpr std::size_t kMaxInFlightSections { 8 }; +constexpr std::size_t kMaxReadySections { 3 }; +constexpr double kStreamStatusLogIntervalSec { 5.0 }; +constexpr double kReenqueueDistanceM { 500.0 }; +constexpr double kCatchupReenqueueDistanceM { 40.0 }; +constexpr double kTeleportReenqueueDistanceM { 2000.0 }; +constexpr float kStationaryCatchupRingThreshold { 0.70f }; +constexpr double kStationaryCatchupSpeedMps { 5.0 }; +constexpr std::size_t kCatchupMaxInFlightSections { 20 }; +constexpr std::size_t kCatchupMaxReadySections { 8 }; +constexpr std::size_t kPackWorkerSubChunkModels { 512 }; +constexpr std::size_t kBootstrapDrainMs { 32 }; +constexpr std::size_t kBootstrapTimeoutMs { 120000 }; +constexpr std::chrono::milliseconds kPresentableHoldMs { 200 }; +constexpr std::chrono::seconds kLoadingScreenMaxBlockSec { 90 }; + +std::optional g_ring_ready_since; +std::optional g_loading_block_started; +bool g_loading_screen_dismissed { false }; +bool g_stream_catchup { false }; +std::size_t g_stream_max_in_flight { kMaxInFlightSections }; +std::size_t g_stream_max_ready { kMaxReadySections }; + +struct PackSectionJob { + int row { 0 }; + int column { 0 }; + std::size_t section_idx { 0 }; + int priority { 0 }; + std::uint64_t resume_byte_offset { 0 }; + std::uint32_t subchunk_index { 0 }; + Eu7PackSectionCursor header_cursor {}; + Eu7PackSectionCursor resume_cursor {}; +}; + +struct PackSectionReady { + int row { 0 }; + int column { 0 }; + std::size_t section_idx { 0 }; + std::unique_ptr> models; + bool failed { false }; + bool section_final { true }; + std::uint32_t subchunk_index { 0 }; +}; + +struct SectionStreamState { + Eu7Module const *module { nullptr }; + std::string path; + simulation::state_serializer *serializer { nullptr }; + std::unordered_set loaded_sections; + std::unordered_set in_flight_sections; + int center_row { -1 }; + int center_column { -1 }; + int radius { kStreamRadius }; + bool active { false }; + bool bootstrap_active { false }; + bool bootstrap_pending { false }; + + std::optional pending_apply; + std::size_t pending_apply_offset { 0 }; + glm::dvec3 last_enqueue_position {}; + bool has_last_enqueue_position { false }; + + std::unordered_map mesh_cache; + std::unordered_map nodedata_cache; + + std::vector workers; + std::atomic worker_exit { false }; + threading::lockable> jobs; + threading::lockable> ready; + threading::condition_variable work_cv; +}; + +SectionStreamState g_stream; + +[[nodiscard]] int +section_manhattan_sections( + int const row, + int const column, + int const center_row, + int const center_column ); + +void +push_prioritized_job( PackSectionJob job ); + +void +reprioritize_job_queue(); + +[[nodiscard]] bool +enqueue_section_if_needed( + int const row, + int const column, + int const priority ); + +void +sync_stream_limits( glm::dvec3 const &world_position ); + +[[nodiscard]] bool +try_dequeue_ready_batch(); + +void +finalize_section( PackSectionReady const &batch ); + +void +fail_section( std::size_t const section_idx ); + +void +release_pending_buffer(); + +[[nodiscard]] double +camera_stream_speed_mps(); + +[[nodiscard]] glm::dvec3 +rotate_velocity_y( glm::dvec3 const &velocity, double const yaw ) { + auto const s { std::sin( yaw ) }; + auto const c { std::cos( yaw ) }; + return { c * velocity.x + s * velocity.z, velocity.y, c * velocity.z - s * velocity.x }; +} + +[[nodiscard]] std::size_t +stream_worker_count() { + auto const hardware { std::thread::hardware_concurrency() }; + auto const threads { hardware > 0 ? hardware : 1u }; + auto const percent { + static_cast( + std::clamp( Global.eu7_pack_stream_workers_percent, 1, 100 ) ) }; + auto const requested { + std::max( + std::size_t { 1 }, + static_cast( threads ) * percent / 100 ) }; + return std::min( requested, kMaxPackStreamWorkers ); +} + +void +reset_stream_fields() { + g_stream.module = nullptr; + g_stream.path.clear(); + g_stream.serializer = nullptr; + g_stream.loaded_sections.clear(); + g_stream.in_flight_sections.clear(); + g_stream.center_row = -1; + g_stream.center_column = -1; + g_stream.radius = kStreamRadius; + g_stream.active = false; + g_stream.bootstrap_active = false; + g_stream.bootstrap_pending = false; + g_stream.pending_apply.reset(); + g_stream.pending_apply_offset = 0; + g_stream.has_last_enqueue_position = false; + g_stream.mesh_cache.clear(); + g_stream.nodedata_cache.clear(); + reset_pack_texture_warm_cache(); +} + +[[nodiscard]] Eu7Module const & +stream_module() { + if( g_stream.module == nullptr ) { + throw std::runtime_error( "EU7 PACK: stream module nie jest ustawiony" ); + } + return *g_stream.module; +} + +[[nodiscard]] std::string const & +pack_stream_file_path() { + if( false == g_stream.path.empty() ) { + return g_stream.path; + } + return stream_module().source_path; +} + +void +release_pending_buffer() { + if( false == g_stream.pending_apply.has_value() ) { + g_stream.pending_apply_offset = 0; + return; + } + + if( g_stream.pending_apply->models != nullptr ) { + g_stream.pending_apply->models->clear(); + g_stream.pending_apply->models->shrink_to_fit(); + g_stream.pending_apply->models.reset(); + } + g_stream.pending_apply.reset(); + g_stream.pending_apply_offset = 0; +} + +[[nodiscard]] std::size_t +ready_queue_size() { + std::lock_guard lock { g_stream.ready.mutex }; + return g_stream.ready.data.size(); +} + +[[nodiscard]] bool +stream_backpressure() { + if( g_stream.bootstrap_active ) { + return false; + } + return ( + g_stream.in_flight_sections.size() >= g_stream_max_in_flight || + ready_queue_size() >= g_stream_max_ready ); +} + +[[nodiscard]] double +camera_stream_speed_mps() { + auto const rotated { + rotate_velocity_y( Global.pCamera.Velocity, Global.pCamera.Angle.y ) }; + return glm::length( rotated ) * 5.0; +} + +[[nodiscard]] glm::dvec3 +camera_travel_forward() { + if( glm::length2( Global.pCamera.Velocity ) > 0.01 ) { + auto const rotated { + rotate_velocity_y( Global.pCamera.Velocity, Global.pCamera.Angle.y ) }; + auto horizontal { glm::dvec3 { rotated.x, 0.0, rotated.z } }; + auto const length { glm::length( horizontal ) }; + if( length > 0.01 ) { + return horizontal / length; + } + } + + auto const yaw { Global.pCamera.Angle.y }; + return glm::dvec3 { std::sin( yaw ), 0.0, std::cos( yaw ) }; +} + +[[nodiscard]] double +gameplay_apply_budget_ms() { + return g_stream_catchup ? kCatchupApplyBudgetMs : kGameplayApplyBudgetMs; +} + +[[nodiscard]] std::size_t +gameplay_slice_instances() { + return g_stream_catchup ? kCatchupSliceInstances : kGameplaySliceInstances; +} + +[[nodiscard]] std::size_t +gameplay_slice_cold_meshes() { + return g_stream_catchup ? kCatchupSliceColdMeshes : kGameplaySliceColdMeshes; +} + +[[nodiscard]] double +gameplay_cold_budget_ms() { + return g_stream_catchup ? kCatchupColdBudgetMs : kGameplayColdBudgetMs; +} + +[[nodiscard]] std::size_t +adaptive_slice_instances( std::size_t const section_total ) { + auto limit { gameplay_slice_instances() }; + if( g_stream_catchup ) { + auto const speed { camera_stream_speed_mps() }; + if( speed > 1200.0 ) { + limit = std::min( limit, kCatchupSliceInstances ); + } + else if( speed > 600.0 ) { + limit = std::min( limit, std::size_t { 96 } ); + } + else if( speed > 300.0 ) { + limit = std::min( limit, std::size_t { 80 } ); + } + else { + limit = std::min( limit, std::size_t { 96 } ); + } + } + + if( section_total > 4000 ) { + limit = std::min( limit, std::size_t { 48 } ); + } + else if( section_total > 2000 ) { + limit = std::min( limit, std::size_t { 64 } ); + } + else if( section_total > 800 ) { + limit = std::min( limit, std::size_t { 80 } ); + } + else if( section_total <= kPackWorkerSubChunkModels ) { + limit = std::min( limit, std::size_t { 64 } ); + if( g_stream_catchup && camera_stream_speed_mps() > 300.0 ) { + limit = std::min( limit, std::size_t { 48 } ); + } + } + + auto const last_ms { pack_bench_stream().last_chunk_ms }; + if( last_ms >= 24.0 ) { + limit = std::min( limit, std::size_t { 32 } ); + } + else if( last_ms >= 12.0 ) { + limit = std::min( limit, std::size_t { 48 } ); + } + + return std::max( limit, std::size_t { 16 } ); +} + +[[nodiscard]] std::size_t +adaptive_cold_meshes() { + auto const &stream { pack_bench_stream() }; + if( stream.last_chunk_ms >= 40.0 || stream.peak_chunk_ms >= 80.0 ) { + return 0; + } + auto limit { gameplay_slice_cold_meshes() }; + if( stream.last_chunk_ms >= 12.0 ) { + limit = 1; + } + return limit; +} + +[[nodiscard]] std::size_t +pending_section_total() { + if( + false == g_stream.pending_apply.has_value() || + g_stream.pending_apply->models == nullptr ) { + return 0; + } + return g_stream.pending_apply->models->size(); +} + +[[nodiscard]] std::size_t +preload_slice_cold_meshes( + scene::eu7::Eu7Model const *const models, + std::size_t const count, + std::size_t const max_cold_meshes, + double const cold_budget_ms, + std::size_t &slice_count ) { + slice_count = count; + if( models == nullptr || count == 0 ) { + slice_count = 0; + return 0; + } + + std::size_t cold_loaded { 0 }; + auto const cold_started { std::chrono::steady_clock::now() }; + for( std::size_t i { 0 }; i < slice_count; ++i ) { + auto model_file { models[ i ].model_file }; + if( model_file.empty() || model_file == "notload" ) { + continue; + } + replace_slashes( model_file ); + if( g_stream.mesh_cache.contains( model_file ) ) { + continue; + } + if( cold_loaded >= max_cold_meshes ) { + slice_count = i; + break; + } + if( cold_budget_ms > 0.0 && cold_loaded > 0 ) { + auto const elapsed_ms { + std::chrono::duration( + std::chrono::steady_clock::now() - cold_started ).count() }; + if( elapsed_ms >= cold_budget_ms ) { + slice_count = i; + break; + } + } + TModel3d *mesh { nullptr }; + { + PackBenchTimer const load_timer { &Eu7PackBench::main_cold_getmodel_ms }; + mesh = TModelsManager::GetModel( model_file, false, false ); + pack_bench_inc( &Eu7PackBench::main_cold_getmodel_calls ); + } + if( mesh != nullptr ) { + TAnimModel::warm_instanceable_cache( mesh ); + } + g_stream.mesh_cache.emplace( model_file, mesh ); + ++cold_loaded; + } + + if( slice_count == 0 && count > 0 ) { + auto model_file { models[ 0 ].model_file }; + if( model_file.empty() || model_file == "notload" ) { + slice_count = 1; + return 0; + } + replace_slashes( model_file ); + if( g_stream.mesh_cache.contains( model_file ) ) { + slice_count = 1; + return 0; + } + if( max_cold_meshes == 0 ) { + return 0; + } + TModel3d *mesh { nullptr }; + { + PackBenchTimer const load_timer { &Eu7PackBench::main_cold_getmodel_ms }; + mesh = TModelsManager::GetModel( model_file, false, false ); + pack_bench_inc( &Eu7PackBench::main_cold_getmodel_calls ); + } + if( mesh != nullptr ) { + TAnimModel::warm_instanceable_cache( mesh ); + } + g_stream.mesh_cache.emplace( model_file, mesh ); + slice_count = 1; + cold_loaded = 1; + } + return cold_loaded; +} + +[[nodiscard]] bool +apply_pending_chunk( + double const budget_ms, + std::size_t const max_instances, + std::size_t const max_cold_meshes, + double const cold_budget_ms, + std::size_t const max_chunks ) { + if( g_stream.serializer == nullptr ) { + return false; + } + if( false == g_stream.pending_apply.has_value() && false == try_dequeue_ready_batch() ) { + return false; + } + + auto const started { std::chrono::steady_clock::now() }; + bool applied_work { false }; + std::size_t chunks_done { 0 }; + + while( chunks_done < max_chunks ) { + if( false == g_stream.pending_apply.has_value() ) { + if( false == try_dequeue_ready_batch() ) { + return applied_work; + } + } + + auto &batch { *g_stream.pending_apply }; + if( batch.failed || batch.models == nullptr || batch.models->empty() ) { + if( batch.failed ) { + fail_section( batch.section_idx ); + } + else { + finalize_section( batch ); + } + release_pending_buffer(); + applied_work = true; + continue; + } + + scene::eu7::Eu7Model const * const models_ptr { batch.models->data() }; + auto const total { batch.models->size() }; + auto const offset { g_stream.pending_apply_offset }; + if( offset >= total ) { + if( batch.section_final ) { + finalize_section( batch ); + } + release_pending_buffer(); + applied_work = true; + continue; + } + + auto const effective_instances { + std::min( max_instances, adaptive_slice_instances( total ) ) }; + auto const effective_cold { + std::min( max_cold_meshes, adaptive_cold_meshes() ) }; + auto const remaining { total - offset }; + auto const chunk_cap { std::min( effective_instances, remaining ) }; + std::size_t chunk_count { chunk_cap }; + + auto const chunk_started { std::chrono::steady_clock::now() }; + double cold_ms { 0.0 }; + double warm_ms { 0.0 }; + double apply_ms { 0.0 }; + std::size_t cold_loads { 0 }; + std::size_t tex_fetches { 0 }; + + { + auto const phase_started { std::chrono::steady_clock::now() }; + cold_loads = preload_slice_cold_meshes( + models_ptr + offset, + chunk_cap, + effective_cold, + cold_budget_ms, + chunk_count ); + cold_ms = std::chrono::duration( + std::chrono::steady_clock::now() - phase_started ).count(); + } + if( chunk_count == 0 ) { + return applied_work; + } + + { + auto const phase_started { std::chrono::steady_clock::now() }; + tex_fetches = warm_pack_textures_main( models_ptr + offset, chunk_count ); + warm_ms = std::chrono::duration( + std::chrono::steady_clock::now() - phase_started ).count(); + } + pack_bench_inc( &Eu7PackBench::main_chunk_tex_fetches, tex_fetches ); + + scene::scratch_data scratch; + simulation::eu7_pack_apply_session const session { + &g_stream.mesh_cache, + &g_stream.nodedata_cache }; + { + auto const phase_started { std::chrono::steady_clock::now() }; + g_stream.serializer->apply_eu7_pack_models( + models_ptr, + offset, + chunk_count, + scratch, + &session ); + apply_ms = std::chrono::duration( + std::chrono::steady_clock::now() - phase_started ).count(); + } + + auto const chunk_wall_ms { + std::chrono::duration( + std::chrono::steady_clock::now() - chunk_started ).count() }; + pack_bench_note_chunk( + chunk_wall_ms, + chunk_count, + cold_loads, + cold_ms, + warm_ms, + apply_ms ); + + if( chunk_wall_ms >= 12.0 && pack_bench_stream_phase_active() ) { + WriteLog( + "EU7 PACK [hitch]: chunk_ms=" + std::to_string( static_cast( chunk_wall_ms ) ) + + " inst=" + std::to_string( chunk_count ) + + " cold=" + std::to_string( cold_loads ) + "(" + std::to_string( static_cast( cold_ms ) ) + "ms)" + + " warm=" + std::to_string( tex_fetches ) + "(" + std::to_string( static_cast( warm_ms ) ) + "ms)" + + " apply=" + std::to_string( static_cast( apply_ms ) ) + "ms" + + " pending=" + std::to_string( offset + chunk_count ) + "/" + std::to_string( total ) + + " sec=" + std::to_string( batch.row ) + "," + std::to_string( batch.column ) ); + } + load_stats().pack_models += chunk_count; + g_stream.pending_apply_offset = offset + chunk_count; + applied_work = true; + ++chunks_done; + + if( g_stream.pending_apply_offset >= total ) { + if( batch.section_final ) { + finalize_section( batch ); + } + release_pending_buffer(); + continue; + } + + if( budget_ms <= 0.0 ) { + return applied_work; + } + + auto const elapsed_ms { + std::chrono::duration( + std::chrono::steady_clock::now() - started ).count() }; + if( elapsed_ms >= budget_ms ) { + pack_bench_inc( &Eu7PackBench::drain_budget_stops ); + return applied_work; + } + } + + return applied_work; +} + +void +maybe_log_stream_status( glm::dvec3 const &world_position ) { + if( false == pack_bench_stream_phase_active() ) { + return; + } + + static double s_last_status_log { 0.0 }; + auto const now { Timer::GetTime() }; + if( now - s_last_status_log < kStreamStatusLogIntervalSec ) { + return; + } + s_last_status_log = now; + + auto const speed { camera_stream_speed_mps() }; + auto const inner_ring { + section_stream_ring_progress( world_position, kSectionStreamGameplayRadiusKm ) }; + auto const outer_ring { + section_stream_ring_progress( world_position, g_stream.radius ) }; + + std::size_t pending_total { 0 }; + std::size_t pending_offset { g_stream.pending_apply_offset }; + if( g_stream.pending_apply.has_value() && g_stream.pending_apply->models != nullptr ) { + pending_total = g_stream.pending_apply->models->size(); + } + + log_pack_stream_status( + world_position, + speed, + inner_ring, + outer_ring, + ready_queue_size(), + g_stream.in_flight_sections.size(), + pending_offset, + pending_total, + gameplay_apply_budget_ms(), + adaptive_slice_instances( pending_total ) ); +} + +[[nodiscard]] bool +section_has_pack_models( int const row, int const column ) { + if( g_stream.module == nullptr ) { + return false; + } + auto const entry { find_pack_entry( *g_stream.module, row, column ) }; + return entry.has_value() && entry->model_count > 0; +} + +[[nodiscard]] bool +is_bootstrap_ring_loaded( + int const center_row, + int const center_column, + int const radius ) { + for( int row { center_row - radius }; row <= center_row + radius; ++row ) { + if( row < 0 || row >= kRegionSideSectionCount ) { + continue; + } + for( int column { center_column - radius }; column <= center_column + radius; ++column ) { + if( column < 0 || column >= kRegionSideSectionCount ) { + continue; + } + if( false == section_has_pack_models( row, column ) ) { + continue; + } + if( false == g_stream.loaded_sections.contains( section_index( row, column ) ) ) { + return false; + } + } + } + return true; +} + +void +worker_loop( std::stop_token const stop_token ) { + while( false == stop_token.stop_requested() && false == g_stream.worker_exit.load() ) { + PackSectionJob job; + bool has_job { false }; + { + std::lock_guard lock { g_stream.jobs.mutex }; + if( false == g_stream.jobs.data.empty() ) { + job = g_stream.jobs.data.front(); + g_stream.jobs.data.pop_front(); + has_job = true; + } + } + + if( false == has_job ) { + g_stream.work_cv.wait_for( std::chrono::milliseconds( 50 ) ); + continue; + } + + if( ready_queue_size() >= g_stream_max_ready ) { + { + std::lock_guard lock { g_stream.jobs.mutex }; + g_stream.jobs.data.push_front( job ); + } + g_stream.work_cv.wait_for( std::chrono::milliseconds( 8 ) ); + continue; + } + + try { + auto const &module { stream_module() }; + auto const entry { find_pack_entry( module, job.row, job.column ) }; + if( false == entry.has_value() || entry->model_count == 0 ) { + PackSectionReady result; + result.row = job.row; + result.column = job.column; + result.section_idx = job.section_idx; + result.failed = true; + result.section_final = true; + { + std::lock_guard lock { g_stream.ready.mutex }; + g_stream.ready.data.push_back( std::move( result ) ); + } + pack_bench_inc( &Eu7PackBench::worker_failures ); + continue; + } + + auto const &pack_path { pack_stream_file_path() }; + if( pack_path.empty() ) { + throw std::runtime_error( "EU7 PACK: brak sciezki pliku PACK" ); + } + + std::ifstream input { pack_path, std::ios::binary }; + if( !input ) { + throw std::runtime_error( + "EU7 PACK: nie mozna otworzyc \"" + pack_path + "\"" ); + } + + Eu7PackSectionCursor header_cursor {}; + Eu7PackSectionCursor cursor {}; + if( job.resume_byte_offset == 0 ) { + seek_pack_section( module, input, *entry, header_cursor ); + cursor = header_cursor; + } + else { + header_cursor = job.header_cursor; + resume_pack_section( + module, + input, + *entry, + job.resume_byte_offset, + job.resume_cursor, + cursor ); + } + + auto batch_index { job.subchunk_index }; + bool section_completed { false }; + for( ;; ) { + if( cursor.solo_remaining == 0 && cursor.inst_remaining == 0 ) { + break; + } + + if( ready_queue_size() >= g_stream_max_ready ) { + PackSectionJob resume_job { job }; + resume_job.header_cursor = header_cursor; + resume_job.resume_byte_offset = static_cast( + input.tellg() - static_cast( + module.pack_payload_offset + entry->pack_offset ) ); + resume_job.subchunk_index = batch_index; + resume_job.resume_cursor = cursor; + { + std::lock_guard lock { g_stream.jobs.mutex }; + g_stream.jobs.data.push_front( std::move( resume_job ) ); + } + g_stream.work_cv.notify_all(); + break; + } + + std::unique_ptr> models; + { + PackBenchTimer const read_timer { &Eu7PackBench::worker_read_pack_ms }; + auto chunk { + read_pack_models_chunk( + module, + input, + cursor, + kPackWorkerSubChunkModels ) }; + if( chunk.empty() ) { + break; + } + models = std::make_unique>( std::move( chunk ) ); + } + + section_completed = + cursor.solo_remaining == 0 && cursor.inst_remaining == 0; + + PackSectionReady result; + result.row = job.row; + result.column = job.column; + result.section_idx = job.section_idx; + result.models = std::move( models ); + result.subchunk_index = batch_index; + result.section_final = section_completed; + + if( result.models != nullptr ) { + pack_bench_inc( + &Eu7PackBench::worker_models_decoded, result.models->size() ); + } + + { + std::lock_guard lock { g_stream.ready.mutex }; + g_stream.ready.data.push_back( std::move( result ) ); + } + + ++batch_index; + + if( section_completed ) { + break; + } + } + + if( section_completed ) { + pack_bench_inc( &Eu7PackBench::worker_sections_done ); + } + } + catch( std::exception const &ex ) { + ErrorLog( + std::string{ "EU7 PACK: sekcja " + std::to_string( job.row ) + "," + + std::to_string( job.column ) + ": " } + ex.what() ); + fail_section( job.section_idx ); + PackSectionReady result; + result.row = job.row; + result.column = job.column; + result.section_idx = job.section_idx; + result.failed = true; + result.section_final = true; + { + std::lock_guard lock { g_stream.ready.mutex }; + g_stream.ready.data.push_back( std::move( result ) ); + } + pack_bench_inc( &Eu7PackBench::worker_failures ); + } + } +} + +void +stop_workers() { + g_stream.worker_exit = true; + g_stream.work_cv.notify_all(); + g_stream.workers.clear(); + g_stream.worker_exit = false; + + { + std::lock_guard lock { g_stream.jobs.mutex }; + g_stream.jobs.data.clear(); + } + { + std::lock_guard lock { g_stream.ready.mutex }; + for( auto &batch : g_stream.ready.data ) { + if( batch.models != nullptr ) { + batch.models->clear(); + batch.models->shrink_to_fit(); + batch.models.reset(); + } + } + g_stream.ready.data.clear(); + } + g_stream.in_flight_sections.clear(); + release_pending_buffer(); + g_stream.bootstrap_active = false; + g_ring_ready_since.reset(); +} + +void +start_workers() { + stop_workers(); + if( false == g_stream.active ) { + return; + } + + auto const worker_count { stream_worker_count() }; + g_stream.workers.reserve( worker_count ); + for( std::size_t worker_idx { 0 }; worker_idx < worker_count; ++worker_idx ) { + g_stream.workers.emplace_back( worker_loop ); + } + + WriteLog( + "EU7 PACK: async loader started, workers=" + std::to_string( worker_count ) + + ", radius=" + std::to_string( kStreamRadius ) + + ", bootstrap=" + std::to_string( kInitialBootstrapRadius ) + "km" + + ", lookahead=" + std::to_string( kMovementLookahead ) ); +} + +void +finalize_section( PackSectionReady const &batch ) { + if( false == batch.section_final ) { + return; + } + g_stream.loaded_sections.insert( batch.section_idx ); + g_stream.in_flight_sections.erase( batch.section_idx ); + ++load_stats().pack_sections_loaded; + pack_bench_inc( &Eu7PackBench::sections_finalized ); +} + +void +fail_section( std::size_t const section_idx ) { + g_stream.loaded_sections.insert( section_idx ); + g_stream.in_flight_sections.erase( section_idx ); +} + +[[nodiscard]] int +section_manhattan_sections( + int const row, + int const column, + int const center_row, + int const center_column ) { + return std::abs( row - center_row ) + std::abs( column - center_column ); +} + +void +push_prioritized_job( PackSectionJob job ) { + std::lock_guard lock { g_stream.jobs.mutex }; + auto const insert_before { + std::find_if( + g_stream.jobs.data.begin(), + g_stream.jobs.data.end(), + [&]( PackSectionJob const &existing ) { + return existing.priority > job.priority; + } ) }; + g_stream.jobs.data.insert( insert_before, std::move( job ) ); +} + +void +reprioritize_job_queue() { + std::lock_guard lock { g_stream.jobs.mutex }; + if( g_stream.jobs.data.empty() ) { + return; + } + + for( auto &job : g_stream.jobs.data ) { + job.priority = section_manhattan_sections( + job.row, + job.column, + g_stream.center_row, + g_stream.center_column ); + } + + std::stable_sort( + g_stream.jobs.data.begin(), + g_stream.jobs.data.end(), + []( PackSectionJob const &lhs, PackSectionJob const &rhs ) { + return lhs.priority < rhs.priority; + } ); +} + +[[nodiscard]] bool +enqueue_section_if_needed( + int const row, + int const column, + int const priority ) { + if( row < 0 || row >= kRegionSideSectionCount || column < 0 || column >= kRegionSideSectionCount ) { + return false; + } + if( stream_backpressure() ) { + return false; + } + + auto const section_idx { section_index( row, column ) }; + if( g_stream.loaded_sections.contains( section_idx ) ) { + return false; + } + if( g_stream.in_flight_sections.contains( section_idx ) ) { + { + std::lock_guard lock { g_stream.jobs.mutex }; + for( auto &queued : g_stream.jobs.data ) { + if( queued.section_idx != section_idx ) { + continue; + } + if( priority < queued.priority ) { + queued.priority = priority; + std::stable_sort( + g_stream.jobs.data.begin(), + g_stream.jobs.data.end(), + []( PackSectionJob const &lhs, PackSectionJob const &rhs ) { + return lhs.priority < rhs.priority; + } ); + } + return false; + } + } + return false; + } + if( + g_stream.pending_apply.has_value() && + g_stream.pending_apply->section_idx == section_idx ) { + return false; + } + + g_stream.in_flight_sections.insert( section_idx ); + + PackSectionJob job; + job.row = row; + job.column = column; + job.section_idx = section_idx; + job.priority = priority; + + push_prioritized_job( std::move( job ) ); + g_stream.work_cv.notify_all(); + return true; +} + +void +enqueue_sections_around( + int const center_row, + int const center_column, + int const ring_radius, + glm::dvec3 const &world_position ) { + struct SectionCandidate { + int row { 0 }; + int column { 0 }; + int distance { 0 }; + }; + + std::vector candidates; + candidates.reserve( static_cast( ( ring_radius * 2 + 1 ) * ( ring_radius * 2 + 1 ) ) ); + + for( int row { center_row - ring_radius }; row <= center_row + ring_radius; ++row ) { + if( row < 0 || row >= kRegionSideSectionCount ) { + continue; + } + for( int column { center_column - ring_radius }; column <= center_column + ring_radius; ++column ) { + if( column < 0 || column >= kRegionSideSectionCount ) { + continue; + } + + auto const section_center_x { + ( static_cast( column ) - kRegionSideSectionCount / 2 ) * kSectionSizeM + + kSectionSizeM * 0.5 }; + auto const section_center_z { + ( static_cast( row ) - kRegionSideSectionCount / 2 ) * kSectionSizeM + + kSectionSizeM * 0.5 }; + auto const dx { world_position.x - section_center_x }; + auto const dz { world_position.z - section_center_z }; + auto const dist { + static_cast( + ( std::abs( dx ) + std::abs( dz ) ) / kSectionSizeM ) }; + + candidates.push_back( { row, column, dist } ); + } + } + + std::sort( + candidates.begin(), + candidates.end(), + []( SectionCandidate const &lhs, SectionCandidate const &rhs ) { + return lhs.distance < rhs.distance; + } ); + + for( auto const &candidate : candidates ) { + auto priority { candidate.distance }; + if( candidate.row == center_row && candidate.column == center_column ) { + priority = 0; + } + enqueue_section_if_needed( candidate.row, candidate.column, priority ); + if( stream_backpressure() ) { + break; + } + } +} + +void +replenish_bootstrap_ring( glm::dvec3 const &world_position ) { + if( world_position.x == 0.0 && world_position.y == 0.0 && world_position.z == 0.0 ) { + return; + } + + auto const [row, column] { section_row_column( world_position ) }; + g_stream.center_row = row; + g_stream.center_column = column; + enqueue_sections_around( row, column, kInitialBootstrapRadius, world_position ); +} + +[[nodiscard]] std::pair +movement_section_delta( glm::dvec3 const &forward_hint ) { + auto const horizontal { glm::dvec3 { forward_hint.x, 0.0, forward_hint.z } }; + auto const length { glm::length( horizontal ) }; + if( length < 0.01 ) { + return { 0, 0 }; + } + + auto const fwd { horizontal / length }; + if( std::abs( fwd.x ) >= std::abs( fwd.z ) ) { + return { 0, fwd.x > 0.0 ? 1 : -1 }; + } + return { fwd.z > 0.0 ? 1 : -1, 0 }; +} + +[[nodiscard]] glm::dvec3 +guess_travel_forward() { + if( + simulation::Train != nullptr && + simulation::Train->Dynamic() != nullptr ) { + auto *const vehicle { simulation::Train->Dynamic() }; + if( std::abs( vehicle->GetVelocity() ) > 0.5 ) { + auto forward { vehicle->VectorFront() }; + if( vehicle->DirectionGet() < 0 ) { + forward = -forward; + } + return forward; + } + } + + if( Global.pCamera.m_owner == nullptr || FreeFlyModeFlag ) { + return camera_travel_forward(); + } + return {}; +} + +void +enqueue_movement_lookahead( + int const center_row, + int const center_column, + int const ring_radius, + glm::dvec3 const &forward_hint, + int const max_steps ) { + auto const [drow, dcolumn] { movement_section_delta( forward_hint ) }; + if( drow == 0 && dcolumn == 0 ) { + return; + } + + auto lookahead_steps { std::min( max_steps, kMovementLookahead ) }; + if( + simulation::Train != nullptr && + simulation::Train->Dynamic() != nullptr ) { + auto const speed { std::abs( simulation::Train->Dynamic()->GetVelocity() ) }; + if( speed > 25.0 ) { + lookahead_steps = std::min( max_steps, 10 ); + } + else if( speed > 15.0 ) { + lookahead_steps = std::min( max_steps, 8 ); + } + else if( speed > 8.0 ) { + lookahead_steps = std::min( max_steps, 6 ); + } + } + if( max_steps > kMovementLookahead ) { + auto const cam_speed { camera_stream_speed_mps() }; + if( cam_speed > 200.0 ) { + lookahead_steps = max_steps; + } + else if( cam_speed > 80.0 ) { + lookahead_steps = std::max( lookahead_steps, std::min( max_steps, 8 ) ); + } + } + + for( int step { 1 }; step <= lookahead_steps; ++step ) { + auto const row { center_row + drow * ( ring_radius + step ) }; + auto const column { center_column + dcolumn * ( ring_radius + step ) }; + auto const priority { + section_manhattan_sections( row, column, center_row, center_column ) }; + if( enqueue_section_if_needed( row, column, priority ) ) { + pack_bench_inc( &Eu7PackBench::stream_lookahead_enqueue ); + } + if( stream_backpressure() ) { + break; + } + } +} + +[[nodiscard]] glm::dvec3 +guess_initial_stream_position( Eu7Module const &root_module ) { + auto const &saved_camera { Global.FreeCameraInit[ 0 ] }; + if( saved_camera.x != 0.0 || saved_camera.y != 0.0 || saved_camera.z != 0.0 ) { + return saved_camera; + } + + if( + false == Global.local_start_vehicle.empty() && + Global.local_start_vehicle != "ghostview" ) { + if( auto *vehicle { simulation::Vehicles.find( Global.local_start_vehicle ) }; + vehicle != nullptr ) { + return vehicle->GetPosition(); + } + } + + for( auto const &trainset : root_module.scene.trainsets ) { + if( trainset.track.empty() ) { + continue; + } + if( auto *track { simulation::Paths.find( trainset.track ) }; track != nullptr ) { + return glm::dvec3 { track->location() }; + } + } + + for( auto *vehicle : simulation::Vehicles.sequence() ) { + if( vehicle != nullptr ) { + return vehicle->GetPosition(); + } + } + + return {}; +} + +[[nodiscard]] bool +try_dequeue_ready_batch() { + if( g_stream.pending_apply.has_value() ) { + return true; + } + + PackSectionReady batch; + bool has_batch { false }; + { + std::lock_guard lock { g_stream.ready.mutex }; + if( false == g_stream.ready.data.empty() ) { + auto const best_it { + std::min_element( + g_stream.ready.data.begin(), + g_stream.ready.data.end(), + []( PackSectionReady const &lhs, PackSectionReady const &rhs ) { + auto const lhs_dist { + section_manhattan_sections( + lhs.row, + lhs.column, + g_stream.center_row, + g_stream.center_column ) }; + auto const rhs_dist { + section_manhattan_sections( + rhs.row, + rhs.column, + g_stream.center_row, + g_stream.center_column ) }; + if( lhs_dist != rhs_dist ) { + return lhs_dist < rhs_dist; + } + if( lhs.section_idx != rhs.section_idx ) { + return lhs.section_idx < rhs.section_idx; + } + return lhs.subchunk_index < rhs.subchunk_index; + } ) }; + batch = std::move( *best_it ); + g_stream.ready.data.erase( best_it ); + has_batch = true; + } + } + + if( false == has_batch ) { + return false; + } + + if( batch.failed || batch.models == nullptr || batch.models->empty() ) { + if( batch.failed ) { + fail_section( batch.section_idx ); + } + else { + finalize_section( batch ); + } + return false; + } + + g_stream.pending_apply = std::move( batch ); + g_stream.pending_apply_offset = 0; + return true; +} + +[[nodiscard]] bool +gameplay_stream_mode() { + return g_loading_screen_dismissed + && false == g_stream.bootstrap_active + && false == g_stream.bootstrap_pending; +} + +void +replenish_bootstrap_ring( glm::dvec3 const &world_position ); + +void +note_drain_idle() { + if( ready_queue_size() > 0 || false == g_stream.in_flight_sections.empty() ) { + pack_bench_inc( &Eu7PackBench::main_drain_wait_worker ); + } + else { + pack_bench_inc( &Eu7PackBench::main_drain_idle ); + } +} + +void +drain_apply_budget( + double const budget_ms, + std::size_t const max_instances, + std::size_t const max_cold_meshes, + double const cold_budget_ms, + std::size_t const max_chunks ) { + if( false == g_stream.active || g_stream.serializer == nullptr || budget_ms <= 0.0 ) { + return; + } + + pack_bench_inc( &Eu7PackBench::main_drain_calls ); + pack_bench_note_ready_queue( ready_queue_size() ); + pack_bench_note_in_flight( g_stream.in_flight_sections.size() ); + + if( false == apply_pending_chunk( + budget_ms, + max_instances, + max_cold_meshes, + cold_budget_ms, + max_chunks ) ) { + note_drain_idle(); + } +} + +void +drain_sections( std::size_t const max_sections ) { + if( false == g_stream.active || g_stream.serializer == nullptr || max_sections == 0 ) { + return; + } + + pack_bench_inc( &Eu7PackBench::main_drain_calls ); + pack_bench_note_ready_queue( ready_queue_size() ); + pack_bench_note_in_flight( g_stream.in_flight_sections.size() ); + + bool applied_work { false }; + for( std::size_t i { 0 }; i < max_sections; ++i ) { + if( false == apply_pending_chunk( + 0.0, + kLoaderSliceInstances, + kLoaderSliceColdMeshes, + 0.0, + std::numeric_limits::max() ) ) { + break; + } + applied_work = true; + } + + if( false == applied_work ) { + note_drain_idle(); + } +} + +void +drain_until_budget( double const budget_ms ) { + if( budget_ms <= 0.0 ) { + return; + } + + auto const started { std::chrono::steady_clock::now() }; + while( true ) { + auto const elapsed_ms { + std::chrono::duration( + std::chrono::steady_clock::now() - started ).count() }; + auto const remaining_ms { budget_ms - elapsed_ms }; + if( remaining_ms <= 0.0 ) { + break; + } + + if( false == apply_pending_chunk( + remaining_ms, + kLoaderSliceInstances, + kLoaderSliceColdMeshes, + 0.0, + std::numeric_limits::max() ) ) { + break; + } + + if( + false == g_stream.pending_apply.has_value() && + ready_queue_size() == 0 ) { + break; + } + } +} + +void +sync_stream_limits( glm::dvec3 const &world_position ) { + if( false == g_loading_screen_dismissed ) { + g_stream_catchup = false; + g_stream_max_in_flight = 6; + g_stream_max_ready = 2; + return; + } + + auto const inner_progress { + section_stream_ring_progress( world_position, kSectionStreamGameplayRadiusKm ) }; + g_stream_catchup = + inner_progress < 0.99f + || false == section_stream_ready_around( + world_position, + kSectionStreamGameplayRadiusKm ); + g_stream_max_in_flight = g_stream_catchup ? + kCatchupMaxInFlightSections : + kMaxInFlightSections; + g_stream_max_ready = g_stream_catchup ? + kCatchupMaxReadySections : + kMaxReadySections; +} + +} // namespace + +void +init_section_stream( + Eu7Module const &root_module, + std::string const &resolved_path, + simulation::state_serializer &serializer ) { + stop_workers(); + reset_stream_fields(); + + g_stream.module = &root_module; + g_stream.path = resolved_path; + g_stream.serializer = &serializer; + g_stream.active = root_module.has_pack_chunk; + + g_stream.mesh_cache.reserve( 1024 ); + g_stream.nodedata_cache.reserve( 1024 ); + + if( g_stream.active ) { + g_loading_block_started = std::chrono::steady_clock::now(); + g_stream.bootstrap_pending = true; + WriteLog( + "EU7 PACK: streaming wlaczony, plik=\"" + resolved_path + "\", sekcji w indeksie=" + + std::to_string( root_module.pack_catalog.entries.size() ) + + ", payload=" + std::to_string( root_module.pack_catalog.pack_payload_size ) + " B" ); + start_workers(); + } +} + +void +prime_section_stream( Eu7Module const &root_module ) { + if( false == g_stream.active ) { + return; + } + + auto const initial { guess_initial_stream_position( root_module ) }; + if( initial.x == 0.0 && initial.y == 0.0 && initial.z == 0.0 ) { + WriteLog( "EU7 PACK: brak pozycji startowej — bootstrap po zaladowaniu scenariusza" ); + return; + } + + auto const [row, column] { section_row_column( initial ) }; + g_stream.center_row = row; + g_stream.center_column = column; + + WriteLog( + "EU7 PACK: pre-warm sekcji " + std::to_string( row ) + "," + std::to_string( column ) + + " (" + std::to_string( initial.x ) + "," + std::to_string( initial.y ) + "," + + std::to_string( initial.z ) + ")" ); + + g_stream.bootstrap_active = true; + enqueue_sections_around( row, column, kInitialBootstrapRadius, initial ); + g_stream.bootstrap_active = false; + g_stream.bootstrap_pending = false; +} + +[[nodiscard]] glm::dvec3 +resolve_stream_position() { + if( + simulation::Train != nullptr && + simulation::Train->Dynamic() != nullptr ) { + return simulation::Train->Dynamic()->GetPosition(); + } + + if( + false == Global.local_start_vehicle.empty() && + Global.local_start_vehicle != "ghostview" ) { + if( auto *vehicle { simulation::Vehicles.find( Global.local_start_vehicle ) }; + vehicle != nullptr ) { + return vehicle->GetPosition(); + } + } + + auto const &saved_camera { Global.FreeCameraInit[ 0 ] }; + if( saved_camera.x != 0.0 || saved_camera.y != 0.0 || saved_camera.z != 0.0 ) { + return saved_camera; + } + + for( auto *vehicle : simulation::Vehicles.sequence() ) { + if( vehicle != nullptr ) { + return vehicle->GetPosition(); + } + } + + return {}; +} + +void +bootstrap_section_stream( glm::dvec3 const &world_position ) { + if( false == g_stream.active ) { + return; + } + + auto const [row, column] { section_row_column( world_position ) }; + g_stream.center_row = row; + g_stream.center_column = column; + + WriteLog( + "EU7 PACK: bootstrap " + std::to_string( kInitialBootstrapRadius ) + "km, sekcja " + + std::to_string( row ) + "," + std::to_string( column ) ); + + auto const started { std::chrono::steady_clock::now() }; + g_stream.bootstrap_active = true; + enqueue_sections_around( row, column, kInitialBootstrapRadius, world_position ); + + while( false == is_bootstrap_ring_loaded( row, column, kInitialBootstrapRadius ) ) { + drain_until_budget( kBootstrapDrainMs ); + + if( false == is_bootstrap_ring_loaded( row, column, kInitialBootstrapRadius ) ) { + enqueue_sections_around( row, column, kInitialBootstrapRadius, world_position ); + } + + auto const elapsed_ms { + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started ).count() }; + if( static_cast( elapsed_ms ) >= kBootstrapTimeoutMs ) { + ErrorLog( "EU7 PACK: bootstrap timeout po " + std::to_string( elapsed_ms ) + " ms" ); + break; + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 8 ) ); + } + + g_stream.bootstrap_active = false; + g_stream.bootstrap_pending = false; + + auto const elapsed_ms { + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started ).count() }; + WriteLog( + "EU7 PACK: bootstrap zakonczony, sekcji=" + + std::to_string( g_stream.loaded_sections.size() ) + ", czas=" + + std::to_string( elapsed_ms ) + " ms" ); +} + +void +update_section_stream( glm::dvec3 const &world_position ) { + if( false == g_stream.active ) { + return; + } + + auto const [center_row, center_column] { section_row_column( world_position ) }; + auto const center_moved { + center_row != g_stream.center_row || center_column != g_stream.center_column }; + auto const moved_since_enqueue { + g_stream.has_last_enqueue_position ? + glm::length( world_position - g_stream.last_enqueue_position ) : + std::numeric_limits::infinity() }; + + if( moved_since_enqueue > kTeleportReenqueueDistanceM ) { + g_stream.has_last_enqueue_position = false; + } + + g_stream.center_row = center_row; + g_stream.center_column = center_column; + + if( center_moved ) { + reprioritize_job_queue(); + } + + sync_stream_limits( world_position ); + + auto const ring_radius { g_stream.radius }; + auto const reenqueue_distance { + g_stream_catchup ? kCatchupReenqueueDistanceM : kReenqueueDistanceM }; + auto max_lookahead { g_stream_catchup ? 15 : 5 }; + if( g_stream_catchup ) { + auto const speed { camera_stream_speed_mps() }; + if( speed > 1200.0 ) { + max_lookahead = 25; + } + else if( speed > 600.0 ) { + max_lookahead = 20; + } + } + auto const travel_forward { guess_travel_forward() }; + + auto const current_section_unloaded { + section_has_pack_models( center_row, center_column ) + && false == g_stream.loaded_sections.contains( section_index( center_row, center_column ) ) }; + auto const inner_ring_incomplete { + current_section_unloaded + || false == section_stream_ready_around( world_position, kSectionStreamGameplayRadiusKm ) }; + auto const should_reenqueue { + center_moved + || false == g_stream.has_last_enqueue_position + || moved_since_enqueue >= reenqueue_distance }; + + if( should_reenqueue || inner_ring_incomplete || g_stream_catchup ) { + enqueue_sections_around( center_row, center_column, ring_radius, world_position ); + g_stream.last_enqueue_position = world_position; + g_stream.has_last_enqueue_position = true; + if( should_reenqueue || inner_ring_incomplete ) { + pack_bench_inc( &Eu7PackBench::stream_reenqueue ); + } + enqueue_movement_lookahead( + center_row, + center_column, + ring_radius, + travel_forward, + max_lookahead ); + } +} + +[[nodiscard]] bool +section_stream_drain_idle( glm::dvec3 const &world_position ) { + if( g_stream.bootstrap_pending || g_stream.bootstrap_active ) { + return false; + } + if( g_stream.pending_apply.has_value() || ready_queue_size() > 0 ) { + return false; + } + if( false == g_stream.in_flight_sections.empty() || g_stream_catchup ) { + return false; + } + if( false == section_stream_ready_around( world_position, kSectionStreamGameplayRadiusKm ) ) { + return false; + } + return load_stats().pack_models >= 500; +} + +void +drain_section_stream( + std::size_t const max_cold_meshes, + std::size_t const max_instances ) { + (void)max_cold_meshes; + (void)max_instances; + + if( false == g_stream.active ) { + return; + } + if( section_stream_drain_idle( Global.pCamera.Pos ) ) { + return; + } + + if( gameplay_stream_mode() ) { + auto apply_budget { gameplay_apply_budget_ms() }; + std::size_t max_chunks { 1 }; + if( g_stream_catchup ) { + auto const inner_ring { + section_stream_ring_progress( + Global.pCamera.Pos, + kSectionStreamGameplayRadiusKm ) }; + auto const speed { camera_stream_speed_mps() }; + if( + inner_ring < kStationaryCatchupRingThreshold + && speed < kStationaryCatchupSpeedMps ) { + max_chunks = 2; + apply_budget = std::max( apply_budget, kCatchupApplyBudgetMs * 1.25 ); + } + } + drain_apply_budget( + apply_budget, + gameplay_slice_instances(), + gameplay_slice_cold_meshes(), + gameplay_cold_budget_ms(), + max_chunks ); + maybe_log_stream_status( Global.pCamera.Pos ); + } + else if( false == g_loading_screen_dismissed ) { + auto position { stream_loading_position() }; + if( section_stream_needs_bootstrap() ) { + kick_section_stream_bootstrap(); + } + else if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { + replenish_bootstrap_ring( position ); + } + drain_until_budget( kLoaderDrainBudgetMs ); + } + else { + drain_until_budget( kDrainBudgetMs ); + } +} + +bool +section_stream_active() { + return g_stream.active; +} + +bool +section_stream_needs_bootstrap() { + return g_stream.active && g_stream.bootstrap_pending; +} + +void +kick_section_stream_bootstrap() { + if( false == section_stream_needs_bootstrap() ) { + return; + } + + auto position { resolve_stream_position() }; + if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { + position = guess_initial_stream_position( stream_module() ); + } + if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { + WriteLog( "EU7 PACK: bootstrap async — brak pozycji startowej" ); + g_stream.bootstrap_pending = false; + return; + } + + auto const [row, column] { section_row_column( position ) }; + g_stream.center_row = row; + g_stream.center_column = column; + + WriteLog( + "EU7 PACK: bootstrap async " + std::to_string( kInitialBootstrapRadius ) + "km, sekcja " + + std::to_string( row ) + "," + std::to_string( column ) ); + + enqueue_sections_around( row, column, kInitialBootstrapRadius, position ); + g_stream.bootstrap_pending = false; +} + +void +try_bootstrap_section_stream() { + if( false == section_stream_needs_bootstrap() ) { + return; + } + + auto position { resolve_stream_position() }; + if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { + position = guess_initial_stream_position( stream_module() ); + } + if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { + return; + } + + bootstrap_section_stream( position ); +} + +void +preload_section_stream( double const max_drain_ms ) { + if( false == g_stream.active ) { + return; + } + + simulation::State.drain_deferred_eu7_trainsets( 16.0 ); + + auto position { resolve_stream_position() }; + if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { + position = guess_initial_stream_position( stream_module() ); + } + + if( section_stream_needs_bootstrap() ) { + if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { + bootstrap_section_stream( position ); + } + } + else if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { + update_section_stream( position ); + } + + auto const started { std::chrono::steady_clock::now() }; + while( true ) { + drain_until_budget( kDrainBudgetMs ); + if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { + update_section_stream( position ); + } + + auto const elapsed_ms { + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started ).count() }; + if( static_cast( elapsed_ms ) >= max_drain_ms ) { + break; + } + if( + g_stream.pending_apply.has_value() || ready_queue_size() > 0 || + false == g_stream.in_flight_sections.empty() ) { + continue; + } + if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { + if( section_stream_ready_around( position, kInitialBootstrapRadius ) ) { + break; + } + } + if( load_stats().pack_models >= 1500 ) { + break; + } + std::this_thread::sleep_for( std::chrono::milliseconds( 8 ) ); + } + + WriteLog( + "EU7 PACK: preload sekcji=" + std::to_string( load_stats().pack_sections_loaded ) + + " modele=" + std::to_string( load_stats().pack_models ) ); +} + +[[nodiscard]] bool +ring_section_in_radius( + int const row, + int const column, + int const center_row, + int const center_column, + int const radius_km ) { + return std::abs( row - center_row ) <= radius_km + && std::abs( column - center_column ) <= radius_km; +} + +[[nodiscard]] float +section_ring_apply_fraction( int const row, int const column ) { + auto const section_idx { section_index( row, column ) }; + + if( g_stream.loaded_sections.contains( section_idx ) ) { + return 1.0f; + } + + if( + g_stream.pending_apply.has_value() && + g_stream.pending_apply->section_idx == section_idx && + g_stream.pending_apply->models != nullptr && + false == g_stream.pending_apply->models->empty() ) { + auto const total { g_stream.pending_apply->models->size() }; + return static_cast( g_stream.pending_apply_offset ) / + static_cast( total ); + } + + { + std::lock_guard lock { g_stream.ready.mutex }; + for( auto const &batch : g_stream.ready.data ) { + if( batch.section_idx == section_idx ) { + return 0.85f; + } + } + } + + if( g_stream.in_flight_sections.contains( section_idx ) ) { + return 0.15f; + } + + return 0.f; +} + +[[nodiscard]] bool +ring_has_pending_pack_work( + int const center_row, + int const center_column, + int const radius_km ) { + for( int row { center_row - radius_km }; row <= center_row + radius_km; ++row ) { + if( row < 0 || row >= kRegionSideSectionCount ) { + continue; + } + for( int column { center_column - radius_km }; column <= center_column + radius_km; ++column ) { + if( column < 0 || column >= kRegionSideSectionCount ) { + continue; + } + if( false == section_has_pack_models( row, column ) ) { + continue; + } + auto const section_idx { section_index( row, column ) }; + if( g_stream.in_flight_sections.contains( section_idx ) ) { + return true; + } + if( + g_stream.pending_apply.has_value() && + g_stream.pending_apply->section_idx == section_idx ) { + return true; + } + } + } + + { + std::lock_guard lock { g_stream.ready.mutex }; + for( auto const &batch : g_stream.ready.data ) { + if( ring_section_in_radius( + batch.row, + batch.column, + center_row, + center_column, + radius_km ) + && section_has_pack_models( batch.row, batch.column ) ) { + return true; + } + } + } + + return false; +} + +bool +section_stream_ready_around( + glm::dvec3 const &world_position, + int const radius_km ) { + if( false == g_stream.active ) { + return true; + } + + auto const [center_row, center_column] { section_row_column( world_position ) }; + if( false == is_bootstrap_ring_loaded( center_row, center_column, radius_km ) ) { + return false; + } + if( g_stream.pending_apply.has_value() ) { + auto const &batch { *g_stream.pending_apply }; + if( ring_section_in_radius( + batch.row, + batch.column, + center_row, + center_column, + radius_km ) + && section_has_pack_models( batch.row, batch.column ) + && batch.models != nullptr ) { + return false; + } + } + if( ring_has_pending_pack_work( center_row, center_column, radius_km ) ) { + return false; + } + if( section_stream_needs_bootstrap() ) { + return false; + } + + return true; +} + +bool +section_stream_presentable_around( + glm::dvec3 const &world_position, + int const radius_km ) { + if( false == section_stream_ready_around( world_position, radius_km ) ) { + g_ring_ready_since.reset(); + return false; + } + + auto const now { std::chrono::steady_clock::now() }; + if( false == g_ring_ready_since.has_value() ) { + g_ring_ready_since = now; + return false; + } + + return ( now - *g_ring_ready_since ) >= kPresentableHoldMs; +} + +bool +loading_screen_dismissed() { + return g_loading_screen_dismissed; +} + +void +dismiss_loading_screen() { + pack_bench_begin_stream_phase(); + g_loading_screen_dismissed = true; + g_ring_ready_since.reset(); +} + +bool +loading_screen_blocks_world( + glm::dvec3 const &world_position, + int const radius_km ) { + if( false == g_stream.active || g_loading_screen_dismissed ) { + return false; + } + + if( g_loading_block_started.has_value() ) { + auto const blocked_for { + std::chrono::steady_clock::now() - *g_loading_block_started }; + if( blocked_for >= kLoadingScreenMaxBlockSec ) { + auto const ring { + section_stream_ring_progress( world_position, radius_km ) }; + ErrorLog( + "EU7 PACK: loading screen timeout — wchodzę w świat (ring=" + + std::to_string( static_cast( ring * 100.f ) ) + "%, ready=" + + std::to_string( section_stream_ready_around( world_position, radius_km ) ? 1 : 0 ) + + ", pending_apply=" + + std::to_string( g_stream.pending_apply.has_value() ? 1 : 0 ) + + ", ready_q=" + std::to_string( ready_queue_size() ) + + ", in_flight=" + std::to_string( g_stream.in_flight_sections.size() ) + ")" ); + dismiss_loading_screen(); + return false; + } + } + + if( + section_stream_ring_progress( world_position, radius_km ) >= 1.0f + && section_stream_ready_around( world_position, radius_km ) ) { + dismiss_loading_screen(); + return false; + } + + if( section_stream_presentable_around( world_position, radius_km ) ) { + dismiss_loading_screen(); + return false; + } + + return true; +} + +glm::dvec3 +stream_loading_position() { + if( Global.pCamera.Pos.x != 0.0 || Global.pCamera.Pos.y != 0.0 || Global.pCamera.Pos.z != 0.0 ) { + return Global.pCamera.Pos; + } + + auto position { resolve_stream_position() }; + if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { + return position; + } + + return Global.FreeCameraInit[ 0 ]; +} + +float +section_stream_ring_progress( + glm::dvec3 const &world_position, + int const radius_km ) { + if( false == g_stream.active ) { + return 1.f; + } + + auto const [center_row, center_column] { section_row_column( world_position ) }; + std::size_t pack_sections { 0 }; + float accumulated { 0.f }; + + for( int row { center_row - radius_km }; row <= center_row + radius_km; ++row ) { + if( row < 0 || row >= kRegionSideSectionCount ) { + continue; + } + for( int column { center_column - radius_km }; column <= center_column + radius_km; ++column ) { + if( column < 0 || column >= kRegionSideSectionCount ) { + continue; + } + if( false == section_has_pack_models( row, column ) ) { + continue; + } + ++pack_sections; + accumulated += section_ring_apply_fraction( row, column ); + } + } + + if( pack_sections == 0 ) { + return 1.f; + } + + return std::clamp( accumulated / static_cast( pack_sections ), 0.f, 1.f ); +} + +void +reset_section_stream() { + flush_pack_stream_bench(); + stop_workers(); + reset_stream_fields(); + g_ring_ready_since.reset(); + g_loading_block_started.reset(); + g_loading_screen_dismissed = false; + g_stream_catchup = false; + g_stream_max_in_flight = kMaxInFlightSections; + g_stream_max_ready = kMaxReadySections; + reset_pack_bench(); +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_section_stream.h b/scene/eu7/eu7_section_stream.h new file mode 100644 index 00000000..a1e2cd92 --- /dev/null +++ b/scene/eu7/eu7_section_stream.h @@ -0,0 +1,105 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include + +namespace simulation { +class state_serializer; +} + +namespace scene::eu7 { + +constexpr int kSectionStreamBootstrapRadiusKm { 3 }; +constexpr int kSectionStreamGameplayRadiusKm { 4 }; + +void +init_section_stream( + Eu7Module const &RootModule, + std::string const &ResolvedPath, + simulation::state_serializer &Serializer ); + +void +prime_section_stream( Eu7Module const &RootModule ); + +[[nodiscard]] glm::dvec3 +resolve_stream_position(); + +// Opcjonalnie blokuje do zaladowania pierścienia bootstrap wokol pozycji (debug/narzedzia). +void +bootstrap_section_stream( glm::dvec3 const &WorldPosition ); + +void +update_section_stream( glm::dvec3 const &WorldPosition ); + +// Worker dekoduje sekcje; watek mapy wstawia po wskazniku do bufora (bez re-read pliku). +void +drain_section_stream( + std::size_t MaxColdMeshes = 0, + std::size_t MaxInstances = 0 ); + +[[nodiscard]] bool +section_stream_active(); + +[[nodiscard]] bool +section_stream_needs_bootstrap(); + +// Po wejsciu w symulacje: kolejkuje bootstrap bez blokowania petli renderu. +void +kick_section_stream_bootstrap(); + +// Blokujacy bootstrap (preload / narzedzia). +void +try_bootstrap_section_stream(); + +// Po zaladowaniu scenariusza: bootstrap + drain PACK przed wejsciem w jazde. +void +preload_section_stream( double MaxDrainMs = 300.0 ); + +// Wszystkie sekcje PACK w pierścieniu RadiusKm wokol pozycji wczytane i zaaplikowane. +[[nodiscard]] bool +section_stream_ready_around( + glm::dvec3 const &WorldPosition, + int RadiusKm = kSectionStreamBootstrapRadiusKm ); + +// ready_around stabilnie przez kilka klatek — bez migania overlay/sceny. +[[nodiscard]] bool +section_stream_presentable_around( + glm::dvec3 const &WorldPosition, + int RadiusKm = kSectionStreamBootstrapRadiusKm ); + +// Renderer/UI: trzymaj czarny ekran + overlay, nie rysuj swiata 3D. +// Jednorazowo przy starcie — po dismiss nie wraca przy streamingu w locie. +[[nodiscard]] bool +loading_screen_blocks_world( + glm::dvec3 const &WorldPosition, + int RadiusKm = kSectionStreamBootstrapRadiusKm ); + +[[nodiscard]] bool +loading_screen_dismissed(); + +void +dismiss_loading_screen(); + +[[nodiscard]] glm::dvec3 +stream_loading_position(); + +// 0..1 — ile sekcji PACK z pierścienia juz siedzi w Region. +[[nodiscard]] float +section_stream_ring_progress( + glm::dvec3 const &WorldPosition, + int RadiusKm = kSectionStreamBootstrapRadiusKm ); + +void +reset_section_stream(); + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_transform.h b/scene/eu7/eu7_transform.h new file mode 100644 index 00000000..0828e4a7 --- /dev/null +++ b/scene/eu7/eu7_transform.h @@ -0,0 +1,258 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/eu7/eu7_types.h" + +#include +#include + +namespace scene::eu7 { + +namespace detail { + +inline constexpr double kPi { 3.14159265358979323846 }; + +struct ScratchLocation { + std::vector offset_stack; + std::vector scale_stack; + glm::dvec3 rotation{}; +}; + +[[nodiscard]] inline ScratchLocation +scratch_from_transform( Eu7TransformContext const &transform ) { + ScratchLocation scratch; + scratch.offset_stack = transform.origin_stack; + scratch.scale_stack = transform.scale_stack; + scratch.rotation = transform.rotation; + return scratch; +} + +} // namespace detail + +[[nodiscard]] inline glm::dvec3 +inverse_transform_point( glm::dvec3 location, Eu7TransformContext const &transform ) { + auto const scratch { detail::scratch_from_transform( transform ) }; + + if( !scratch.offset_stack.empty() ) { + auto const &off { scratch.offset_stack.back() }; + location.x -= off.x; + location.y -= off.y; + location.z -= off.z; + } + + if( !scratch.scale_stack.empty() ) { + auto const &sc { scratch.scale_stack.back() }; + if( sc.x != 0.0 ) { + location.x /= sc.x; + } + if( sc.y != 0.0 ) { + location.y /= sc.y; + } + if( sc.z != 0.0 ) { + location.z /= sc.z; + } + } + + if( scratch.rotation.y != 0.0 ) { + auto const rad { -scratch.rotation.y * ( detail::kPi / 180.0 ) }; + auto const c { std::cos( rad ) }; + auto const s { std::sin( rad ) }; + auto const x { location.x * c + location.z * s }; + auto const z { -location.x * s + location.z * c }; + location.x = x; + location.z = z; + } + + return location; +} + +inline void +inverse_transform_shape_vertices( + std::vector &vertices, + Eu7TransformContext const &transform ) { + auto const scratch { detail::scratch_from_transform( transform ) }; + + if( !scratch.offset_stack.empty() ) { + auto const &off { scratch.offset_stack.back() }; + for( auto &vtx : vertices ) { + vtx.position.x -= off.x; + vtx.position.y -= off.y; + vtx.position.z -= off.z; + } + } + + if( scratch.rotation.x != 0.0 || scratch.rotation.y != 0.0 || scratch.rotation.z != 0.0 ) { + auto const rx { -scratch.rotation.x * ( detail::kPi / 180.0 ) }; + auto const ry { -scratch.rotation.y * ( detail::kPi / 180.0 ) }; + auto const rz { -scratch.rotation.z * ( detail::kPi / 180.0 ) }; + + auto rotate_y = [&]( glm::dvec3 &v ) { + auto const c { std::cos( ry ) }; + auto const s { std::sin( ry ) }; + auto const x { v.x * c + v.z * s }; + auto const z { -v.x * s + v.z * c }; + v.x = x; + v.z = z; + }; + auto rotate_x = [&]( glm::dvec3 &v ) { + auto const c { std::cos( rx ) }; + auto const s { std::sin( rx ) }; + auto const y { v.y * c - v.z * s }; + auto const z { v.y * s + v.z * c }; + v.y = y; + v.z = z; + }; + auto rotate_z = [&]( glm::dvec3 &v ) { + auto const c { std::cos( rz ) }; + auto const s { std::sin( rz ) }; + auto const x { v.x * c - v.y * s }; + auto const y { v.x * s + v.y * c }; + v.x = x; + v.y = y; + }; + + for( auto &vtx : vertices ) { + rotate_y( vtx.position ); + rotate_x( vtx.position ); + rotate_z( vtx.position ); + glm::dvec3 n { vtx.normal.x, vtx.normal.y, vtx.normal.z }; + rotate_y( n ); + rotate_x( n ); + rotate_z( n ); + vtx.normal = glm::vec3( static_cast( n.x ), static_cast( n.y ), static_cast( n.z ) ); + } + } +} + +[[nodiscard]] inline bool +transform_is_empty( Eu7TransformContext const &transform ) { + return ( + transform.origin_stack.empty() && + transform.scale_stack.empty() && + transform.rotation.x == 0.0 && + transform.rotation.y == 0.0 && + transform.rotation.z == 0.0 && + transform.group_depth == 0 ); +} + +[[nodiscard]] inline glm::dvec3 +transform_point( glm::dvec3 location, Eu7TransformContext const &transform ) { + if( transform.rotation.y != 0.0 ) { + auto const rad { transform.rotation.y * ( detail::kPi / 180.0 ) }; + auto const c { std::cos( rad ) }; + auto const s { std::sin( rad ) }; + auto const x { location.x * c + location.z * s }; + auto const z { -location.x * s + location.z * c }; + location.x = x; + location.z = z; + } + + if( !transform.scale_stack.empty() ) { + auto const &sc { transform.scale_stack.back() }; + if( sc.x != 0.0 ) { + location.x *= sc.x; + } + if( sc.y != 0.0 ) { + location.y *= sc.y; + } + if( sc.z != 0.0 ) { + location.z *= sc.z; + } + } + + if( !transform.origin_stack.empty() ) { + auto const &off { transform.origin_stack.back() }; + location.x += off.x; + location.y += off.y; + location.z += off.z; + } + + return location; +} + +// Wzgledne wektory kontrolne toru (cp_out/cp_in) — rotacja i skala, bez translacji origin. +[[nodiscard]] inline glm::dvec3 +transform_vector( glm::dvec3 vector, Eu7TransformContext const &transform ) { + if( transform.rotation.y != 0.0 ) { + auto const rad { transform.rotation.y * ( detail::kPi / 180.0 ) }; + auto const c { std::cos( rad ) }; + auto const s { std::sin( rad ) }; + auto const x { vector.x * c + vector.z * s }; + auto const z { -vector.x * s + vector.z * c }; + vector.x = x; + vector.z = z; + } + + if( !transform.scale_stack.empty() ) { + auto const &sc { transform.scale_stack.back() }; + if( sc.x != 0.0 ) { + vector.x *= sc.x; + } + if( sc.y != 0.0 ) { + vector.y *= sc.y; + } + if( sc.z != 0.0 ) { + vector.z *= sc.z; + } + } + + return vector; +} + +inline void +compose_node_transform( Eu7TransformContext &node, Eu7TransformContext const &prefix ) { + if( transform_is_empty( prefix ) ) { + return; + } + + auto const parent_origin { + prefix.origin_stack.empty() ? + glm::dvec3{} : + prefix.origin_stack.back() }; + for( auto &offset : node.origin_stack ) { + offset.x += parent_origin.x; + offset.y += parent_origin.y; + offset.z += parent_origin.z; + } + + auto const parent_scale { + prefix.scale_stack.empty() ? + glm::dvec3{ 1.0, 1.0, 1.0 } : + prefix.scale_stack.back() }; + for( auto &scale : node.scale_stack ) { + scale.x *= parent_scale.x; + scale.y *= parent_scale.y; + scale.z *= parent_scale.z; + } + + node.rotation.x += prefix.rotation.x; + node.rotation.y += prefix.rotation.y; + node.rotation.z += prefix.rotation.z; + + node.group_depth += prefix.group_depth; +} + +void +compose_scene_with_include_prefix( Eu7Scene &scene, Eu7TransformContext const &prefix ); + +[[nodiscard]] inline glm::dvec3 +subtract_origin_offset( glm::dvec3 const &world, Eu7TransformContext const &transform ) { + glm::dvec3 local { world }; + if( !transform.origin_stack.empty() ) { + auto const &off { transform.origin_stack.back() }; + local.x -= off.x; + local.y -= off.y; + local.z -= off.z; + } + return local; +} + +} // namespace scene::eu7 diff --git a/scene/eu7/eu7_types.h b/scene/eu7/eu7_types.h new file mode 100644 index 00000000..cbd50790 --- /dev/null +++ b/scene/eu7/eu7_types.h @@ -0,0 +1,393 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace scene::eu7 { + +struct Eu7Vec4 { + float x = 0.f; + float y = 0.f; + float z = 0.f; + float w = 1.f; +}; + +struct Eu7WorldVertex { + glm::dvec3 position{}; + glm::vec3 normal{}; + double u = 0.0; + double v = 0.0; +}; + +struct Eu7LightingData { + Eu7Vec4 diffuse{ 0.8f, 0.8f, 0.8f, 1.f }; + Eu7Vec4 ambient{ 0.2f, 0.2f, 0.2f, 1.f }; + Eu7Vec4 specular{ 0.f, 0.f, 0.f, 1.f }; +}; + +struct Eu7BoundingArea { + glm::dvec3 center{}; + float radius = -1.f; +}; + +struct Eu7TransformContext { + std::vector origin_stack; + std::vector scale_stack; + glm::dvec3 rotation{}; + std::size_t group_depth = 0; +}; + +struct Eu7BasicNode { + Eu7BoundingArea area; + double range_squared_min = 0.0; + double range_squared_max = std::numeric_limits::max(); + bool visible = true; + std::string name; + std::string node_type; + std::size_t group_handle = 0; + bool group_valid = false; + Eu7TransformContext transform; +}; + +struct Eu7SegmentPath { + glm::dvec3 p_start{}; + double roll_start = 0.0; + glm::dvec3 cp_out{}; + glm::dvec3 cp_in{}; + glm::dvec3 p_end{}; + double roll_end = 0.0; + double radius = 0.0; +}; + +enum class Eu7TrackType : std::uint8_t { + Unknown, + Normal, + Switch, + Table, + Cross, + Tributary, +}; + +enum class Eu7TrackCategory : std::uint8_t { + Rail = 1, + Road = 2, + Water = 4, +}; + +enum class Eu7TrackEnvironment : std::int8_t { + Unknown = -1, + Flat = 0, + Mountains, + Canyon, + Tunnel, + Bridge, + Bank, +}; + +struct Eu7TrackVisibility { + std::string material1; + float tex_length = 4.f; + std::string material2; + float tex_height1 = 0.f; + float tex_width = 0.f; + float tex_slope = 0.f; +}; + +struct Eu7Track { + Eu7BasicNode node; + Eu7TrackType track_type = Eu7TrackType::Unknown; + Eu7TrackCategory category = Eu7TrackCategory::Rail; + float length = 0.f; + float track_width = 0.f; + float friction = 0.f; + float sound_distance = 0.f; + int quality_flag = 0; + int damage_flag = 0; + Eu7TrackEnvironment environment = Eu7TrackEnvironment::Unknown; + std::optional visibility; + std::vector paths; + std::vector> tail_keywords; +}; + +enum class Eu7TractionWireMaterial : std::uint8_t { + None = 0, + Copper = 1, + Aluminium = 2, +}; + +struct Eu7Traction { + Eu7BasicNode node; + std::string power_supply_name; + float nominal_voltage = 0.f; + float max_current = 0.f; + float resistivity_ohm_per_m = 0.f; + double resistivity_legacy = 0.0; + std::string material_raw = "cu"; + Eu7TractionWireMaterial material = Eu7TractionWireMaterial::Copper; + float wire_thickness = 0.f; + int damage_flag = 0; + glm::dvec3 wire_p1{}; + glm::dvec3 wire_p2{}; + glm::dvec3 wire_p3{}; + glm::dvec3 wire_p4{}; + double min_height = 0.0; + double segment_length = 0.0; + int wire_count = 0; + float wire_offset = 0.f; + std::optional parallel_name; +}; + +enum class Eu7PowerSourceModifier : std::uint8_t { + None, + Recuperation, + Section, +}; + +struct Eu7TractionPowerSource { + Eu7BasicNode node; + glm::dvec3 position{}; + float nominal_voltage = 0.f; + float voltage_frequency = 0.f; + double internal_resistance_legacy = 0.2; + float internal_resistance = 0.2f; + float max_output_current = 0.f; + float fast_fuse_timeout = 0.f; + float fast_fuse_repetition = 0.f; + float slow_fuse_timeout = 0.f; + Eu7PowerSourceModifier modifier = Eu7PowerSourceModifier::None; +}; + +struct Eu7Shape { + Eu7BasicNode node; + bool translucent = false; + std::string material_path; + Eu7LightingData lighting; + glm::dvec3 origin{}; + std::vector vertices; +}; + +struct Eu7Lines { + Eu7BasicNode node; + Eu7LightingData lighting; + float line_width = 1.f; + glm::dvec3 origin{}; + std::vector vertices; +}; + +struct Eu7Model { + Eu7BasicNode node; + glm::dvec3 location{}; + glm::dvec3 angles{}; + glm::dvec3 scale{ 1.0, 1.0, 1.0 }; + std::string model_file; + std::string texture_file; + std::vector light_states; + std::vector light_colors; + bool transition = true; + bool is_terrain = false; +}; + +// Wspolna definicja modelu (EU7B v8 PROT) — bez transformacji instancji. +struct Eu7ModelPrototype { + Eu7BasicNode node; + std::string model_file; + std::string texture_file; + std::vector light_states; + std::vector light_colors; + bool transition = true; + bool is_terrain = false; +}; + +struct Eu7MemCell { + Eu7BasicNode node; + std::string text; + double value1 = 0.0; + double value2 = 0.0; + std::optional track_name; +}; + +struct Eu7EventLauncherCondition { + std::string memcell_name; + std::string compare_text; + double compare_value1 = 0.0; + double compare_value2 = 0.0; + int check_mask = 0; +}; + +struct Eu7EventLauncher { + Eu7BasicNode node; + glm::dvec3 location{}; + double radius_squared = 0.0; + std::string activation_key_raw; + int activation_key = 0; + double delta_time = -1.0; + std::string event1_name; + std::string event2_name; + std::optional condition; + bool train_triggered = false; + int launch_hour = -1; + int launch_minute = -1; +}; + +struct Eu7Dynamic { + Eu7BasicNode node; + std::string data_folder; + std::string skin_file; + std::string mmd_file; + std::string track_name; + double offset = -1.0; + std::string driver_type; + int coupling = 3; + std::string coupling_raw = "3"; + std::string coupling_params; + float velocity = 0.f; + int load_count = 0; + std::string load_type; + std::optional destination; + std::optional trainset_index; +}; + +struct Eu7Sound { + Eu7BasicNode node; + glm::dvec3 location{}; + std::string wav_file; +}; + +enum class Eu7EventType : std::uint8_t { + AddValues, + UpdateValues, + CopyValues, + GetValues, + PutValues, + Whois, + LogValues, + Multiple, + Switch, + TrackVel, + Sound, + Texture, + Animation, + Lights, + Voltage, + Visible, + Friction, + Message, + Unknown, +}; + +struct Eu7Event { + std::string name; + Eu7EventType type = Eu7EventType::Unknown; + double delay = 0.0; + std::vector targets; + double delay_random = 0.0; + double delay_departure = 0.0; + bool ignored = false; + bool passive = false; + std::vector> payload; +}; + +struct Eu7Trainset { + std::string name; + std::string track; + float offset = 0.f; + float velocity = 0.f; + std::unordered_map assignment; + std::vector vehicle_indices; + std::vector couplings; + std::size_t driver_index = static_cast( -1 ); +}; + +struct Eu7IncludePlacement { + 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; + } +}; + +struct Eu7Include { + std::uint32_t source_line = 0; + std::string source_path; + std::string binary_path; + std::vector parameters; + Eu7TransformContext site_transform; +}; + +struct Eu7Scene { + std::vector tracks; + std::vector traction; + std::vector power_sources; + std::vector shapes; + std::vector terrain_shapes; + std::vector lines; + std::vector models; + std::vector memcells; + std::vector event_launchers; + std::vector dynamics; + std::vector sounds; + std::vector trainsets; + std::vector events; + std::uint32_t first_init_count = 0; +}; + +// Wpis indeksu PACK: sekcja 1 km x 1 km -> offset w payload chunku PACK. +struct Eu7PackIndexEntry { + std::uint16_t row = 0; + std::uint16_t column = 0; + std::uint32_t model_count = 0; + std::uint64_t pack_offset = 0; +}; + +// Stan inkrementalnego odczytu sekcji PACK z istream (po seek_pack_section). +struct Eu7PackSectionCursor { + std::uint32_t solo_remaining { 0 }; + std::uint32_t inst_remaining { 0 }; + std::uint32_t models_read { 0 }; + std::uint32_t model_total { 0 }; + std::uint8_t section_format { 0 }; + bool header_parsed { false }; +}; + +struct Eu7PackCatalog { + std::vector entries; + std::unordered_map index_by_section; + std::uint64_t pack_payload_size = 0; +}; + +struct Eu7Module { + std::vector includes; + Eu7Scene scene; + Eu7IncludePlacement include_placement; + Eu7PackCatalog pack_catalog; + std::vector model_prototypes; + std::vector strings; + std::string source_path; + std::uint64_t pack_payload_offset = 0; + std::uint32_t version = 0; + bool has_terrain_chunk = false; + bool has_pack_chunk = false; +}; + +} // namespace scene::eu7 diff --git a/scene/scene.cpp b/scene/scene.cpp index c2100449..738578d5 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -359,6 +359,21 @@ basic_cell::insert( TAnimModel *Instance ) { m_active = true; + if( Instance->m_eu7_pack && Instance->m_instanceable ) { + m_instancesopaque.emplace_back( Instance ); + if( Instance->Model() != nullptr ) { + instance_bucket_key key; + key.pModel = Instance->Model(); + auto const *mat = Instance->Material(); + if( mat != nullptr ) { + for( int i = 0; i < 5; ++i ) { key.skins[ i ] = mat->replacable_skins[ i ]; } + } + m_instancebuckets_opaque[ key ].emplace_back( Instance ); + } + enclose_area( Instance ); + return; + } + auto const flags = Instance->Flags(); auto alpha = ( Instance->Material() != nullptr ? diff --git a/scene/scenenode.h b/scene/scenenode.h index 10be9c1b..2f424dda 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -74,11 +74,22 @@ struct node_data { std::string type; }; +class shape_node; +class lines_node; + +namespace eu7 { +struct Eu7Shape; +struct Eu7Lines; +shape_node build_shape_node( Eu7Shape const &Source ); +lines_node build_lines_node( Eu7Lines const &Source ); +} // namespace eu7 + // holds unique piece of geometry, covered with single material class shape_node { friend class basic_region; // region might want to modify node content when it's being inserted + friend shape_node eu7::build_shape_node( eu7::Eu7Shape const & ); public: // types @@ -176,6 +187,7 @@ shape_node::data() const { class lines_node { friend class basic_region; // region might want to modify node content when it's being inserted + friend lines_node eu7::build_lines_node( eu7::Eu7Lines const & ); public: // types diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 51b7945f..45baa49e 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -1,4 +1,4 @@ -/* +/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulationtime.h" #include "simulation/simulationenvironment.h" +#include "scene/eu7/eu7_loader.h" #include "utilities/Globals.h" #include "world/Event.h" #include "world/MemCell.h" @@ -72,6 +73,16 @@ state_manager::export_as_text( std::string const &Scenariofile ) const { return m_serializer.export_as_text( Scenariofile ); } +bool +state_manager::load_eu7_module( std::string const &path ) { + return scene::eu7::load_module( path, m_serializer ); +} + +void +state_manager::drain_deferred_eu7_trainsets( double const max_ms ) { + m_serializer.drain_deferred_eu7_trainsets( max_ms ); +} + void state_manager::init_scripting_interface() { diff --git a/simulation/simulation.h b/simulation/simulation.h index 09ea4d42..a4add91a 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -1,4 +1,4 @@ -/* +/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not @@ -58,6 +58,11 @@ public: // stores class data in specified file, in legacy (text) format void export_as_text( std::string const &Scenariofile ) const; + // laduje pelny modul EU7B (wszystkie chunki) + bool + load_eu7_module( std::string const &Path ); + void + drain_deferred_eu7_trainsets( double MaxMs = 12.0 ); private: // members diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 7277e338..e86ba3ae 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -10,6 +10,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "simulation/simulationstateserializer.h" +#include + #include "utilities/Globals.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" @@ -22,14 +24,570 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/Driver.h" #include "vehicle/DynObj.h" #include "model/AnimModel.h" +#include "model/MdlMngr.h" #include "rendering/lightarray.h" #include "world/TractionPower.h" #include "application/application.h" #include "rendering/renderer.h" #include "utilities/Logs.h" +#include "scene/eu7/eu7_bake.h" +#include "scene/eu7/eu7_loader.h" +#include "scene/eu7/eu7_load_stats.h" +#include "scene/eu7/eu7_pack_bench.h" +#include "scene/eu7/eu7_transform.h" +#include "world/Track.h" +#include "world/Traction.h" +#include "audio/sound.h" + +#include +#include namespace simulation { +namespace { + +constexpr double kDeferTrainsetHorizDistM { 4000.0 }; +constexpr double kDeferTrainsetHorizDistSq { + kDeferTrainsetHorizDistM * kDeferTrainsetHorizDistM }; + +struct DeferredEu7Trainset { + scene::eu7::Eu7Trainset trainset; + std::vector vehicles; +}; + +std::deque g_deferred_eu7_trainsets; + +struct Eu7TransformState { + std::size_t group_depth { 0 }; +}; + +void +clear_deferred_eu7_trainsets() { + g_deferred_eu7_trainsets.clear(); +} + +[[nodiscard]] glm::dvec3 +eu7_trainset_spawn_origin() { + auto const &saved_camera { Global.FreeCameraInit[ 0 ] }; + if( saved_camera.x != 0.0 || saved_camera.y != 0.0 || saved_camera.z != 0.0 ) { + return saved_camera; + } + + if( + false == Global.local_start_vehicle.empty() && + Global.local_start_vehicle != "ghostview" ) { + if( auto *vehicle { simulation::Vehicles.find( Global.local_start_vehicle ) }; + vehicle != nullptr ) { + return vehicle->GetPosition(); + } + } + + for( auto *vehicle : simulation::Vehicles.sequence() ) { + if( vehicle != nullptr ) { + return vehicle->GetPosition(); + } + } + + return {}; +} + +[[nodiscard]] bool +eu7_trainset_is_player( + scene::eu7::Eu7Trainset const &trainset, + scene::eu7::Eu7Scene const &scene ) { + if( + Global.local_start_vehicle.empty() || + Global.local_start_vehicle == "ghostview" ) { + return false; + } + + for( auto const index : trainset.vehicle_indices ) { + if( + index < scene.dynamics.size() && + scene.dynamics[ index ].node.name == Global.local_start_vehicle ) { + return true; + } + } + return false; +} + +[[nodiscard]] double +eu7_trainset_horiz_dist_sq( scene::eu7::Eu7Trainset const &trainset ) { + auto *path { simulation::Paths.find( trainset.track ) }; + if( path == nullptr ) { + return 0.0; + } + + auto const spawn { eu7_trainset_spawn_origin() }; + if( spawn.x == 0.0 && spawn.y == 0.0 && spawn.z == 0.0 ) { + return 0.0; + } + + auto const track_pos { glm::dvec3 { path->location() } }; + auto const dx { track_pos.x - spawn.x }; + auto const dz { track_pos.z - spawn.z }; + return dx * dx + dz * dz; +} + +[[nodiscard]] bool +eu7_should_load_trainset_now( + scene::eu7::Eu7Trainset const &trainset, + scene::eu7::Eu7Scene const &scene ) { + if( eu7_trainset_is_player( trainset, scene ) ) { + return true; + } + if( eu7_trainset_horiz_dist_sq( trainset ) <= kDeferTrainsetHorizDistSq ) { + return true; + } + return false; +} + +void +eu7_queue_deferred_trainset( + scene::eu7::Eu7Trainset const &trainset, + scene::eu7::Eu7Scene const &scene ) { + DeferredEu7Trainset job; + job.trainset = trainset; + job.vehicles.reserve( trainset.vehicle_indices.size() ); + for( auto const index : trainset.vehicle_indices ) { + if( index < scene.dynamics.size() ) { + job.vehicles.push_back( scene.dynamics[ index ] ); + } + } + g_deferred_eu7_trainsets.push_back( std::move( job ) ); +} + +[[nodiscard]] glm::dvec3 +origin_push_delta( std::vector const &stack, std::size_t const index ) { + auto const &cumulative { stack[ index ] }; + if( index == 0 ) { + return cumulative; + } + auto const &parent { stack[ index - 1 ] }; + return { cumulative.x - parent.x, cumulative.y - parent.y, cumulative.z - parent.z }; +} + +[[nodiscard]] glm::vec3 +scale_push_factor( std::vector const &stack, std::size_t const index ) { + auto const &cumulative { stack[ index ] }; + auto const parent { index == 0 ? glm::dvec3{ 1.0, 1.0, 1.0 } : stack[ index - 1 ] }; + return { + parent.x != 0.0 ? static_cast( cumulative.x / parent.x ) : static_cast( cumulative.x ), + parent.y != 0.0 ? static_cast( cumulative.y / parent.y ) : static_cast( cumulative.y ), + parent.z != 0.0 ? static_cast( cumulative.z / parent.z ) : static_cast( cumulative.z ) }; +} + +void +sync_scratch_transform( + scene::scratch_data &scratchpad, + Eu7TransformState &state, + scene::eu7::Eu7TransformContext const &target ) { + while( state.group_depth > target.group_depth ) { + scene::Groups.close(); + --state.group_depth; + } + while( scratchpad.location.scale.size() > target.scale_stack.size() ) { + scratchpad.location.scale.pop(); + } + while( scratchpad.location.offset.size() > target.origin_stack.size() ) { + scratchpad.location.offset.pop(); + } + + while( scratchpad.location.offset.size() < target.origin_stack.size() ) { + auto const index { scratchpad.location.offset.size() }; + auto const delta { origin_push_delta( target.origin_stack, index ) }; + scratchpad.location.offset.push( + delta + ( + scratchpad.location.offset.empty() ? + glm::dvec3{} : + scratchpad.location.offset.top() ) ); + } + + while( scratchpad.location.scale.size() < target.scale_stack.size() ) { + auto const index { scratchpad.location.scale.size() }; + auto const &cumulative { target.scale_stack[ index ] }; + scratchpad.location.scale.push( glm::vec3( + static_cast( cumulative.x ), + static_cast( cumulative.y ), + static_cast( cumulative.z ) ) ); + } + + while( state.group_depth < target.group_depth ) { + scene::Groups.create(); + ++state.group_depth; + } + + scratchpad.location.rotation = glm::vec3( + static_cast( target.rotation.x ), + static_cast( target.rotation.y ), + static_cast( target.rotation.z ) ); +} + +[[nodiscard]] scene::node_data +node_data_from_eu7( scene::eu7::Eu7BasicNode const &node ) { + scene::node_data nodedata; + if( node.range_squared_max >= std::numeric_limits::max() * 0.5 ) { + nodedata.range_max = -1.0; + } + else { + nodedata.range_max = std::sqrt( node.range_squared_max ); + } + nodedata.range_min = std::sqrt( node.range_squared_min ); + nodedata.name = node.name; + nodedata.type = node.node_type; + return nodedata; +} + +void +preload_unique_pack_meshes( + std::vector const &models, + std::size_t const offset, + std::size_t const end ) { + std::unordered_set unique_files; + unique_files.reserve( end - offset ); + + for( std::size_t i { offset }; i < end; ++i ) { + auto model_file { models[ i ].model_file }; + if( model_file.empty() || model_file == "notload" ) { + continue; + } + replace_slashes( model_file ); + if( unique_files.insert( model_file ).second ) { + if( auto *const model { TModelsManager::GetModel( model_file, false, false ) } ) { + TAnimModel::warm_instanceable_cache( model ); + } + } + } +} + +[[nodiscard]] bool +pack_model_needs_full_load( scene::eu7::Eu7Model const &model ) { + return false == model.light_states.empty() + || false == model.light_colors.empty() + || false == model.transition; +} + +[[nodiscard]] std::string +pack_nodedata_cache_key( scene::eu7::Eu7Model const &model ) { + return model.model_file + '\x1f' + model.texture_file + '\x1f' + + std::to_string( model.node.range_squared_min ) + '\x1f' + + std::to_string( model.node.range_squared_max ) + '\x1f' + + ( model.is_terrain ? '1' : '0' ); +} + +[[nodiscard]] scene::node_data const & +pack_nodedata_cached( + scene::eu7::Eu7Model const &model, + std::unordered_map &cache ) { + auto const key { pack_nodedata_cache_key( model ) }; + auto const found { cache.find( key ) }; + if( found != cache.end() ) { + return found->second; + } + scene::node_data nodedata; + if( model.is_terrain ) { + nodedata.range_max = -1.0; + nodedata.range_min = 0.0; + nodedata.name = model.node.name; + nodedata.type = "model"; + } + else { + nodedata = node_data_from_eu7( model.node ); + } + auto const inserted { cache.emplace( key, std::move( nodedata ) ) }; + return inserted.first->second; +} + +[[nodiscard]] std::string +join_event_targets( std::vector const &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; +} + +} // namespace + +struct state_serializer::eu7_transform_state { + Eu7TransformState state; +}; + +void +state_serializer::insert_eu7_models( + std::vector const &models, + scene::scratch_data &scratchpad, + eu7_transform_state &transform_state ) { + scene::eu7::ScopedTimer const model_timer { scene::eu7::load_stats().model_ms }; + scene::eu7::load_stats().models += models.size(); + + auto const apply_node_transform { [&]( scene::eu7::Eu7BasicNode const &node ) { + sync_scratch_transform( scratchpad, transform_state.state, node.transform ); + } }; + + for( auto const &model : models ) { + apply_node_transform( model.node ); + scene::node_data nodedata; + if( model.is_terrain ) { + nodedata.range_max = -1.0; + nodedata.range_min = 0.0; + nodedata.name = model.node.name; + nodedata.type = "model"; + } + else { + nodedata = node_data_from_eu7( model.node ); + } + + auto const local_location { + scene::eu7::inverse_transform_point( model.location, model.node.transform ) }; + auto const local_rotation_y { model.angles.y - model.node.transform.rotation.y }; + auto *instance { new TAnimModel( nodedata ) }; + instance->Angles( scratchpad.location.rotation + glm::vec3( 0.f, static_cast( local_rotation_y ), 0.f ) ); + if( false == scratchpad.location.scale.empty() ) { + instance->Scale( scratchpad.location.scale.top() ); + } + if( model.scale.x != 1.0 || model.scale.y != 1.0 || model.scale.z != 1.0 ) { + instance->Scale( instance->Scale() * glm::vec3( + static_cast( model.scale.x ), + static_cast( model.scale.y ), + static_cast( model.scale.z ) ) ); + } + + auto model_file { model.model_file }; + auto texture_file { model.texture_file }; + replace_slashes( model_file ); + replace_slashes( texture_file ); + std::string load_tokens { model_file + " " + texture_file }; + if( false == model.light_states.empty() ) { + load_tokens += " lights"; + for( auto const state : model.light_states ) { + load_tokens += ' ' + std::to_string( state ); + } + } + if( false == model.light_colors.empty() ) { + load_tokens += " lightcolors"; + for( auto const color : model.light_colors ) { + load_tokens += ' ' + std::to_string( color ); + } + } + if( false == model.transition ) { + load_tokens += " notransition"; + } + load_tokens += " endmodel"; + + cParser model_parser( load_tokens, cParser::buffer_TEXT, "", false ); + if( false == instance->Load( &model_parser, nodedata.range_min < 0.0 ) ) { + SafeDelete( instance ); + continue; + } + instance->location( transform( local_location, scratchpad ) ); + + if( nodedata.range_min < 0.0 ) { + if( false == scratchpad.binary.terrain ) { + auto const cellcount { instance->TerrainCount() + 1 }; + for( auto i = 1; i < cellcount; ++i ) { + auto *submodel { instance->TerrainSquare( i - 1 ) }; + simulation::Region->insert( + scene::shape_node().convert( submodel ), + scratchpad, + false ); + submodel = submodel->ChildGet(); + while( submodel != nullptr ) { + simulation::Region->insert( + scene::shape_node().convert( submodel ), + scratchpad, + false ); + submodel = submodel->NextGet(); + } + } + } + delete instance; + continue; + } + + if( instance->Model() != nullptr ) { + for( auto const &smokesource : instance->Model()->smoke_sources() ) { + Particles.insert( smokesource.first, instance, smokesource.second ); + } + } + if( false == simulation::Instances.insert( instance ) ) { + ErrorLog( "Bad EU7: duplicate model name \"" + instance->name() + "\"" ); + } + scene::Groups.insert( scene::Groups.handle(), instance ); + simulation::Region->insert( instance ); + if( auto *hierarchy_node = static_cast( instance ) ) { + scene::Hierarchy[ hierarchy_node->uuid.to_string() ] = hierarchy_node; + } + } +} + +void +state_serializer::insert_eu7_pack_models( + scene::eu7::Eu7Model const *models, + std::size_t const count, + scene::scratch_data &scratchpad, + eu7_pack_apply_session const *const session ) { + if( models == nullptr || count == 0 ) { + return; + } + + scene::eu7::ScopedTimer const model_timer { scene::eu7::load_stats().model_ms }; + scene::eu7::load_stats().models += count; + + std::unordered_map local_nodedata_cache; + local_nodedata_cache.reserve( std::min( count, std::size_t { 64 } ) ); + std::unordered_map local_mesh_cache; + local_mesh_cache.reserve( std::min( count, std::size_t { 64 } ) ); + + auto &nodedata_cache { + session != nullptr && session->nodedata_cache != nullptr ? + *session->nodedata_cache : + local_nodedata_cache }; + auto &mesh_cache { + session != nullptr && session->mesh_cache != nullptr ? + *session->mesh_cache : + local_mesh_cache }; + + for( std::size_t i { 0 }; i < count; ++i ) { + auto const &model { models[ i ] }; + auto const &nodedata { pack_nodedata_cached( model, nodedata_cache ) }; + + auto *instance { TAnimModel::acquire_pack_instance( nodedata ) }; + instance->Angles( glm::vec3( + static_cast( model.angles.x ), + static_cast( model.angles.y ), + static_cast( model.angles.z ) ) ); + if( model.scale.x != 1.0 || model.scale.y != 1.0 || model.scale.z != 1.0 ) { + instance->Scale( glm::vec3( + static_cast( model.scale.x ), + static_cast( model.scale.y ), + static_cast( model.scale.z ) ) ); + } + + instance->location( glm::vec3( + static_cast( model.location.x ), + static_cast( model.location.y ), + static_cast( model.location.z ) ) ); + + if( nodedata.range_min < 0.0 ) { + if( false == instance->LoadEu7( + model.model_file, + model.texture_file, + model.light_states, + model.light_colors, + model.transition, + true ) ) { + TAnimModel::release_pack_instance( instance ); + continue; + } + if( false == scratchpad.binary.terrain ) { + auto const cellcount { instance->TerrainCount() + 1 }; + for( auto i = 1; i < cellcount; ++i ) { + auto *submodel { instance->TerrainSquare( i - 1 ) }; + simulation::Region->insert( + scene::shape_node().convert( submodel ), + scratchpad, + false ); + submodel = submodel->ChildGet(); + while( submodel != nullptr ) { + simulation::Region->insert( + scene::shape_node().convert( submodel ), + scratchpad, + false ); + submodel = submodel->NextGet(); + } + } + } + TAnimModel::release_pack_instance( instance ); + continue; + } + + bool const needs_full_load { pack_model_needs_full_load( model ) }; + bool loaded { false }; + if( needs_full_load ) { + scene::eu7::PackBenchTimer const load_timer { + &scene::eu7::Eu7PackBench::main_load_eu7_full_ms }; + loaded = instance->LoadEu7( + model.model_file, + model.texture_file, + model.light_states, + model.light_colors, + model.transition, + false ); + if( loaded ) { + scene::eu7::pack_bench_inc( &scene::eu7::Eu7PackBench::main_pack_full_loads ); + } + } + else { + scene::eu7::PackBenchTimer const load_timer { + &scene::eu7::Eu7PackBench::main_load_eu7_pack_ms }; + auto model_file { model.model_file }; + auto texture_file { model.texture_file }; + replace_slashes( model_file ); + replace_slashes( texture_file ); + TModel3d *mesh { nullptr }; + if( false == model_file.empty() && model_file != "notload" ) { + auto const found { mesh_cache.find( model_file ) }; + if( found != mesh_cache.end() ) { + mesh = found->second; + } + else { + mesh = TModelsManager::GetModel( model_file, false, false ); + mesh_cache.emplace( model_file, mesh ); + } + } + loaded = instance->LoadEu7PackWarm( mesh, texture_file ); + if( loaded ) { + scene::eu7::pack_bench_inc( &scene::eu7::Eu7PackBench::main_pack_fast_loads ); + } + } + if( false == loaded ) { + TAnimModel::release_pack_instance( instance ); + continue; + } + + if( auto *const mesh { instance->Model() } ) { + if( false == mesh->smoke_sources().empty() ) { + for( auto const &smokesource : mesh->smoke_sources() ) { + Particles.insert( smokesource.first, instance, smokesource.second ); + } + } + } + { + scene::eu7::PackBenchTimer const region_timer { + &scene::eu7::Eu7PackBench::main_region_insert_ms }; + simulation::Region->insert( instance ); + scene::eu7::pack_bench_inc( &scene::eu7::Eu7PackBench::main_region_inserts ); + scene::eu7::pack_bench_inc( &scene::eu7::Eu7PackBench::main_instances_applied ); + } + } +} + +void +state_serializer::insert_eu7_pack_models( + std::vector const &models, + scene::scratch_data &scratchpad, + std::size_t const offset, + std::size_t const count, + eu7_pack_apply_session const *const session ) { + if( offset >= models.size() || count == 0 ) { + return; + } + + auto const end { std::min( offset + count, models.size() ) }; + insert_eu7_pack_models( models.data() + offset, end - offset, scratchpad, session ); +} + +void +state_serializer::insert_eu7_pack_models( + std::vector const &models, + scene::scratch_data &scratchpad ) { + insert_eu7_pack_models( models, scratchpad, 0, models.size(), nullptr ); +} + std::shared_ptr state_serializer::deserialize_begin( std::string const &Scenariofile ) { @@ -41,71 +599,95 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { simulation::State.init_scripting_interface(); - // NOTE: for the time being import from text format is a given, since we don't have full binary serialization - std::shared_ptr state = - std::make_shared(Scenariofile, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction); + scene::eu7::begin_load_session(); + clear_deferred_eu7_trainsets(); + + auto const resolved_scenario { scene::eu7::resolve_scenery_path( Scenariofile ) }; + bool is_pure_eu7_scenario { scene::eu7::probe_file( resolved_scenario ) }; + + if( false == is_pure_eu7_scenario ) { + auto const bake { scene::eu7::ensure_scenario_eu7( Scenariofile ) }; + if( false == bake.ok ) { + ErrorLog( "EU7: nie udalo sie przygotowac modulu: " + bake.message ); + throw invalid_scenery_exception(); + } + if( bake.regenerated ) { + WriteLog( "EU7: wygenerowano modul scenariusza (" + bake.message + ")" ); + } + is_pure_eu7_scenario = scene::eu7::probe_file( resolved_scenario ); + } + + bool const has_baked_eu7_root { + false == is_pure_eu7_scenario && + scene::eu7::should_use_binary_module( Scenariofile ) }; + auto const baked_root_path { + scene::eu7::resolve_scenery_path( scene::eu7::binary_path( Scenariofile ) ) }; + + // Scenariusz EU7B: nie parsujemy binarki jako tekstu SCM. + std::shared_ptr state; + if( is_pure_eu7_scenario ) { + state = std::make_shared( + std::string{}, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction ); + state->scenariofile = Scenariofile; + } + else { + state = std::make_shared( + Scenariofile, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction ); + } - // TODO: check first for presence of serialized binary files - // if this fails, fall back on the legacy text format state->scratchpad.name = Scenariofile; - if( ( true == Global.file_binary_terrain ) - && ( Scenariofile != "$.scn" ) ) { - // compilation to binary file isn't supported for rainsted-created overrides - // NOTE: we postpone actual loading of the scene until we process time, season and weather data - state->scratchpad.binary.terrain = Region->is_scene( Scenariofile ) ; + + if( is_pure_eu7_scenario ) { + Global.file_binary_terrain_state = true; + state->scratchpad.binary.terrain = true; + state->scratchpad.binary.terrain_included = true; + WriteLog( "EU7 scenario: " + resolved_scenario ); + if( false == scene::eu7::load_module( resolved_scenario, *this ) ) { + throw invalid_scenery_exception(); + } + } + else if( has_baked_eu7_root ) { + Global.file_binary_terrain_state = true; + state->scratchpad.binary.terrain = true; + if( scene::eu7::is_scenario_terrain( Scenariofile ) ) { + state->scratchpad.binary.terrain_included = true; + } + WriteLog( + "EU7 hybrid: metadata z \"" + Scenariofile + "\", sceneria z \"" + + baked_root_path + "\"" ); + if( false == scene::eu7::load_module( baked_root_path, *this ) ) { + throw invalid_scenery_exception(); + } + } + else if( ( true == Global.file_binary_terrain ) + && ( Scenariofile != "$.scn" ) ) { + // EU7 ma pierwszenstwo przed SBT przy tym samym stemie. + if( scene::eu7::is_scenario_terrain( Scenariofile ) ) { + state->scratchpad.binary.terrain = true; + Global.file_binary_terrain_state = true; + WriteLog( "Default EU7 terrain present" ); + } + else if( Region->is_scene( Scenariofile ) ) { + state->scratchpad.binary.terrain = true; + Global.file_binary_terrain_state = true; + WriteLog( "Default SBT present" ); + } + else { + Global.file_binary_terrain_state = false; + WriteLog( "Default binary terrain absent" ); + } + } + else { + Global.file_binary_terrain_state = false; + WriteLog( "Default binary terrain absent" ); } - if (false != state->scratchpad.binary.terrain) - { - Global.file_binary_terrain_state = true; - WriteLog("Default SBT present"); - } - else - { - Global.file_binary_terrain_state = false; - WriteLog("Default SBT absent"); - } scene::Groups.create(); if( false == state->input.ok() ) throw invalid_scenery_exception(); - // prepare deserialization function table - // since all methods use the same objects, we can have simple, hard-coded binds or lambdas for the task - using deserializefunction = void( state_serializer::*)(cParser &, scene::scratch_data &); - std::vector< - std::pair< - std::string, - deserializefunction> > functionlist = { - { "area", &state_serializer::deserialize_area }, - { "isolated", &state_serializer::deserialize_isolated }, - { "assignment", &state_serializer::deserialize_assignment }, - { "atmo", &state_serializer::deserialize_atmo }, - { "camera", &state_serializer::deserialize_camera }, - { "config", &state_serializer::deserialize_config }, - { "description", &state_serializer::deserialize_description }, - { "event", &state_serializer::deserialize_event }, - { "lua", &state_serializer::deserialize_lua }, - { "firstinit", &state_serializer::deserialize_firstinit }, - { "group", &state_serializer::deserialize_group }, - { "endgroup", &state_serializer::deserialize_endgroup }, - { "light", &state_serializer::deserialize_light }, - { "node", &state_serializer::deserialize_node }, - { "origin", &state_serializer::deserialize_origin }, - { "endorigin", &state_serializer::deserialize_endorigin }, - { "scale", &state_serializer::deserialize_scale }, - { "endscale", &state_serializer::deserialize_endscale }, - { "rotate", &state_serializer::deserialize_rotate }, - { "sky", &state_serializer::deserialize_sky }, - { "test", &state_serializer::deserialize_test }, - { "time", &state_serializer::deserialize_time }, - { "trainset", &state_serializer::deserialize_trainset }, - { "terrain", &state_serializer::deserialize_terrain }, - { "endtrainset", &state_serializer::deserialize_endtrainset } }; - - for( auto &function : functionlist ) { - state->functionmap.emplace( function.first, std::bind( function.second, this, std::ref( state->input ), std::ref( state->scratchpad ) ) ); - } + populate_deserialize_functionmap( state->functionmap, state->input, state->scratchpad ); if (!Global.prepend_scn.empty()) { state->input.injectString(Global.prepend_scn); @@ -152,17 +734,845 @@ state_serializer::deserialize_continue(std::shared_ptr state scene::Groups.update_map(); Region->create_map_geometry(); - if( ( true == Global.file_binary_terrain ) - && ( false == state->scratchpad.binary.terrain ) - && ( state->scenariofile != "$.scn" ) ) { - // if we didn't find usable binary version of the scenario files, create them now for future use - // as long as the scenario file wasn't rainsted-created base file override - Region->serialize( state->scenariofile ); - } - return false; } +void +state_serializer::populate_deserialize_functionmap( + std::unordered_map &functionmap, + cParser &input, + scene::scratch_data &scratchpad ) { + using deserializefunction = void( state_serializer::*)( cParser &, scene::scratch_data & ); + std::vector> const functionlist { + { "area", &state_serializer::deserialize_area }, + { "isolated", &state_serializer::deserialize_isolated }, + { "assignment", &state_serializer::deserialize_assignment }, + { "atmo", &state_serializer::deserialize_atmo }, + { "camera", &state_serializer::deserialize_camera }, + { "config", &state_serializer::deserialize_config }, + { "description", &state_serializer::deserialize_description }, + { "event", &state_serializer::deserialize_event }, + { "lua", &state_serializer::deserialize_lua }, + { "firstinit", &state_serializer::deserialize_firstinit }, + { "group", &state_serializer::deserialize_group }, + { "endgroup", &state_serializer::deserialize_endgroup }, + { "light", &state_serializer::deserialize_light }, + { "node", &state_serializer::deserialize_node }, + { "origin", &state_serializer::deserialize_origin }, + { "endorigin", &state_serializer::deserialize_endorigin }, + { "scale", &state_serializer::deserialize_scale }, + { "endscale", &state_serializer::deserialize_endscale }, + { "rotate", &state_serializer::deserialize_rotate }, + { "sky", &state_serializer::deserialize_sky }, + { "test", &state_serializer::deserialize_test }, + { "time", &state_serializer::deserialize_time }, + { "trainset", &state_serializer::deserialize_trainset }, + { "terrain", &state_serializer::deserialize_terrain }, + { "endtrainset", &state_serializer::deserialize_endtrainset } }; + + functionmap.clear(); + for( auto const &function : functionlist ) { + functionmap.emplace( + function.first, + std::bind( function.second, this, std::ref( input ), std::ref( scratchpad ) ) ); + } +} + +void +state_serializer::deserialize_parser_tokens( + cParser &input, + scene::scratch_data &scratchpad, + std::string const &source_name ) { + std::unordered_map functionmap; + populate_deserialize_functionmap( functionmap, input, scratchpad ); + + std::string token { input.getToken() }; + while( false == token.empty() ) { + auto const lookup { functionmap.find( token ) }; + if( lookup != functionmap.end() ) { + lookup->second(); + } + else { + ErrorLog( + "Bad EU7 module: unexpected token \"" + token + "\" in \"" + source_name + + "\" (line " + std::to_string( input.Line() - 1 ) + ")" ); + } + token = input.getToken(); + } +} + +void +state_serializer::deserialize_module_text( + std::string const &text, + std::string const &source_name, + scene::scratch_data &scratchpad ) { + // mPath musi byc katalogiem scenerii (jak przy buffer_FILE), nie pelna sciezka .eu7 — + // inaczej include "td/foo.scm" skleja sie w "scenery/td.eu7td/foo.scm". + cParser input( text, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction ); + // Dzieci INCL sa juz zaladowane w load_module_recursive — bez ponownego include→EU7. + input.expandIncludes = false; + + deserialize_parser_tokens( input, scratchpad, source_name ); +} + +void +state_serializer::deserialize_include_file( + std::string const &include_reference, + std::string const ¤t_relative_file, + std::vector const ¶meters, + scene::scratch_data &scratchpad ) { + cParser input( + include_reference, + cParser::buffer_FILE, + Global.asCurrentSceneryPath, + Global.bLoadTraction, + parameters ); + input.expandIncludes = true; + + deserialize_parser_tokens( input, scratchpad, current_relative_file + " -> " + include_reference ); +} + +void +state_serializer::apply_eu7_models( + std::vector const &models, + scene::scratch_data &scratchpad ) { + eu7_transform_state transform_state; + insert_eu7_models( models, scratchpad, transform_state ); +} + +void +state_serializer::apply_eu7_pack_models( + std::vector const &models, + scene::scratch_data &scratchpad ) { + preload_unique_pack_meshes( models, 0, models.size() ); + insert_eu7_pack_models( models, scratchpad ); +} + +void +state_serializer::apply_eu7_pack_models( + std::vector const &models, + scene::scratch_data &scratchpad, + std::size_t const offset, + std::size_t const count, + eu7_pack_apply_session const *const session ) { + if( session == nullptr && offset < models.size() && count > 0 ) { + auto const end { std::min( offset + count, models.size() ) }; + preload_unique_pack_meshes( models, offset, end ); + } + if( offset >= models.size() || count == 0 ) { + return; + } + auto const end { std::min( offset + count, models.size() ) }; + insert_eu7_pack_models( models.data() + offset, end - offset, scratchpad, session ); +} + +void +state_serializer::apply_eu7_pack_models( + scene::eu7::Eu7Model const *models, + std::size_t const count, + scene::scratch_data &scratchpad, + eu7_pack_apply_session const *const session ) { + insert_eu7_pack_models( models, count, scratchpad, session ); +} + +void +state_serializer::apply_eu7_pack_models( + scene::eu7::Eu7Model const *models, + std::size_t const offset, + std::size_t const count, + scene::scratch_data &scratchpad, + eu7_pack_apply_session const *const session ) { + if( models == nullptr || count == 0 ) { + return; + } + insert_eu7_pack_models( models + offset, count, scratchpad, session ); +} + +void +state_serializer::apply_eu7_scene( + scene::eu7::Eu7Scene const &scene, + scene::scratch_data &scratchpad ) { + eu7_transform_state transform_state; + + auto const scratch_offset { [&]() -> glm::dvec3 { + return ( + scratchpad.location.offset.empty() ? + glm::dvec3{} : + scratchpad.location.offset.top() ); + } }; + + auto const apply_node_transform { [&]( scene::eu7::Eu7BasicNode const &node ) { + sync_scratch_transform( scratchpad, transform_state.state, node.transform ); + } }; + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().trak_ms }; + scene::eu7::load_stats().tracks += scene.tracks.size(); + for( auto const &track : scene.tracks ) { + apply_node_transform( track.node ); + auto const nodedata { node_data_from_eu7( track.node ) }; + auto *path { new TTrack( nodedata ) }; + path->LoadFromEu7( track ); + if( false == simulation::Paths.insert( path ) ) { + ErrorLog( "Bad EU7: duplicate track name \"" + path->name() + "\"" ); + } + scene::Groups.insert( scene::Groups.handle(), path ); + simulation::Region->insert_and_register( path ); + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().trac_ms }; + scene::eu7::load_stats().traction += scene.traction.size(); + for( auto const &traction : scene.traction ) { + if( false == Global.bLoadTraction ) { + continue; + } + apply_node_transform( traction.node ); + auto const nodedata { node_data_from_eu7( traction.node ) }; + auto *piece { new TTraction( nodedata ) }; + auto const origin { scratch_offset() }; + auto const local_p1 { scene::eu7::subtract_origin_offset( traction.wire_p1, traction.node.transform ) + origin }; + auto const local_p2 { scene::eu7::subtract_origin_offset( traction.wire_p2, traction.node.transform ) + origin }; + auto const local_p3 { scene::eu7::subtract_origin_offset( traction.wire_p3, traction.node.transform ) + origin }; + auto const local_p4 { scene::eu7::subtract_origin_offset( traction.wire_p4, traction.node.transform ) + origin }; + + piece->asPowerSupplyName = traction.power_supply_name; + piece->NominalVoltage = traction.nominal_voltage; + piece->MaxCurrent = traction.max_current; + piece->fResistivity = ( + traction.resistivity_legacy != 0.0 ? + static_cast( traction.resistivity_legacy ) : + traction.resistivity_ohm_per_m / 0.001f ); + if( piece->fResistivity == 0.01f ) { + piece->fResistivity = 0.075f; + } + piece->fResistivity *= 0.001f; + + auto const material { ( + traction.material_raw.empty() ? + ( traction.material == scene::eu7::Eu7TractionWireMaterial::Aluminium ? "al" : + traction.material == scene::eu7::Eu7TractionWireMaterial::None ? "none" : "cu" ) : + traction.material_raw ) }; + if( material == "none" ) { piece->Material = 0; } + else if( material == "al" ) { piece->Material = 2; } + else { piece->Material = 1; } + + piece->WireThickness = traction.wire_thickness; + piece->DamageFlag = traction.damage_flag; + piece->pPoint1 = local_p1; + piece->pPoint2 = local_p2; + piece->pPoint3 = local_p3; + piece->pPoint4 = local_p4; + piece->fHeightDifference = ( local_p3.y - local_p1.y + local_p4.y - local_p2.y ) * 0.5 - traction.min_height; + piece->iNumSections = ( + traction.segment_length ? + static_cast( glm::length( local_p1 - local_p2 ) / traction.segment_length ) : + 0 ); + piece->Wires = traction.wire_count; + piece->WireOffset = traction.wire_offset; + piece->m_visible = traction.node.visible; + if( traction.parallel_name ) { + piece->asParallel = *traction.parallel_name; + } + piece->Init(); + piece->location( glm::mix( local_p2, local_p1, 0.5 ) ); + + if( false == simulation::Traction.insert( piece ) ) { + ErrorLog( "Bad EU7: duplicate traction name \"" + piece->name() + "\"" ); + } + scene::Groups.insert( scene::Groups.handle(), piece ); + simulation::Region->insert_and_register( piece ); + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().power_ms }; + scene::eu7::load_stats().power_sources += scene.power_sources.size(); + for( auto const &source : scene.power_sources ) { + if( false == Global.bLoadTraction ) { + continue; + } + apply_node_transform( source.node ); + auto const nodedata { node_data_from_eu7( source.node ) }; + auto const local_position { + scene::eu7::inverse_transform_point( source.position, source.node.transform ) }; + auto const internal_res { ( + source.internal_resistance_legacy != 0.2 ? + source.internal_resistance_legacy : + static_cast( source.internal_resistance ) ) }; + + std::ostringstream power_body; + power_body + << local_position.x << ' ' << local_position.y << ' ' << local_position.z << ' ' + << source.nominal_voltage << ' ' << source.voltage_frequency << ' ' + << internal_res << ' ' << source.max_output_current << ' ' + << source.fast_fuse_timeout << ' ' << source.fast_fuse_repetition << ' ' + << source.slow_fuse_timeout << ' '; + if( source.modifier == scene::eu7::Eu7PowerSourceModifier::Recuperation ) { + power_body << "recuperation "; + } + else if( source.modifier == scene::eu7::Eu7PowerSourceModifier::Section ) { + power_body << "section "; + } + power_body << "end"; + + cParser power_parser( power_body.str(), cParser::buffer_TEXT, "", false ); + auto *powersource { deserialize_tractionpowersource( power_parser, scratchpad, nodedata ) }; + if( powersource == nullptr ) { + continue; + } + + if( false == simulation::Powergrid.insert( powersource ) ) { + ErrorLog( "Bad EU7: duplicate power source name \"" + powersource->name() + "\"" ); + } + } + } + + if( false == scene.models.empty() ) { + if( scene::eu7::pack_scenery_active() ) { + scene::eu7::load_stats().pack_skipped_models += scene.models.size(); + } + else { + insert_eu7_models( scene.models, scratchpad, transform_state ); + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().memcell_ms }; + scene::eu7::load_stats().memcells += scene.memcells.size(); + for( auto const &cell : scene.memcells ) { + apply_node_transform( cell.node ); + auto const nodedata { node_data_from_eu7( cell.node ) }; + auto const local_position { + scene::eu7::inverse_transform_point( cell.node.area.center, cell.node.transform ) }; + + std::ostringstream memcell_body; + memcell_body + << local_position.x << ' ' << local_position.y << ' ' << local_position.z << ' ' + << cell.text << ' ' << cell.value1 << ' ' << cell.value2 << ' ' + << ( cell.track_name ? *cell.track_name : "none" ) + << " endmemcell"; + + cParser memcell_parser( memcell_body.str(), cParser::buffer_TEXT, "", false ); + auto *memorycell { deserialize_memorycell( memcell_parser, scratchpad, nodedata ) }; + if( memorycell == nullptr ) { + continue; + } + + if( false == simulation::Memory.insert( memorycell ) ) { + ErrorLog( "Bad EU7: duplicate memcell name \"" + memorycell->name() + "\"" ); + } + scene::Groups.insert( scene::Groups.handle(), memorycell ); + simulation::Region->insert( memorycell ); + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().launcher_ms }; + scene::eu7::load_stats().launchers += scene.event_launchers.size(); + for( auto const &launcher : scene.event_launchers ) { + apply_node_transform( launcher.node ); + auto const nodedata { node_data_from_eu7( launcher.node ) }; + auto const local_location { + scene::eu7::inverse_transform_point( launcher.location, launcher.node.transform ) }; + + std::string const time_token { ( + launcher.launch_hour != 0 || launcher.launch_minute != 0 ) ? + std::to_string( launcher.launch_hour * 100 + launcher.launch_minute ) : + ( launcher.delta_time != 0.0 ) ? + std::to_string( -launcher.delta_time ) : + "0" }; + + std::ostringstream launcher_body; + launcher_body + << std::sqrt( launcher.radius_squared ) << ' ' + << launcher.activation_key_raw << ' ' + << time_token << ' ' + << launcher.event1_name << ' '; + if( false == launcher.event2_name.empty() && launcher.event2_name != "none" && + launcher.event2_name != "endeventlauncher" ) { + launcher_body << launcher.event2_name << ' '; + } + if( launcher.condition ) { + launcher_body + << "condition " + << launcher.condition->memcell_name << ' ' + << launcher.condition->compare_text << ' '; + if( launcher.condition->check_mask & 2 ) { + launcher_body << launcher.condition->compare_value1 << ' '; + } + else { + launcher_body << "* "; + } + if( launcher.condition->check_mask & 4 ) { + launcher_body << launcher.condition->compare_value2 << ' '; + } + else { + launcher_body << "* "; + } + } + if( launcher.train_triggered ) { + launcher_body << "traintriggered "; + } + launcher_body << "endeventlauncher"; + + cParser launcher_parser( launcher_body.str(), cParser::buffer_TEXT, "", false ); + auto *eventlauncher { new TEventLauncher( nodedata ) }; + eventlauncher->Load( &launcher_parser ); + eventlauncher->location( transform( local_location, scratchpad ) ); + + if( false == simulation::Events.insert( eventlauncher ) ) { + ErrorLog( "Bad EU7: duplicate event launcher name \"" + eventlauncher->name() + "\"" ); + continue; + } + if( true == eventlauncher->IsGlobal() ) { + simulation::Events.queue( eventlauncher ); + } + else { + scene::Groups.insert( scene::Groups.handle(), eventlauncher ); + if( false == eventlauncher->IsRadioActivated() ) { + simulation::Region->insert( eventlauncher ); + } + } + } + } + + std::vector used_in_trainset( scene.dynamics.size(), false ); + for( auto const &trainset : scene.trainsets ) { + for( auto const index : trainset.vehicle_indices ) { + if( index < used_in_trainset.size() ) { + used_in_trainset[ index ] = true; + } + } + } + + auto const apply_dynamic { [&]( scene::eu7::Eu7Dynamic const &vehicle, bool const in_trainset ) { + apply_node_transform( vehicle.node ); + auto const nodedata { node_data_from_eu7( vehicle.node ) }; + if( false == in_trainset ) { + scratchpad.trainset = scene::scratch_data::trainset_data(); + } + + auto datafolder { vehicle.data_folder }; + auto skinfile { vehicle.skin_file }; + auto mmdfile { vehicle.mmd_file }; + replace_slashes( datafolder ); + replace_slashes( skinfile ); + replace_slashes( mmdfile ); + + auto const pathname { ( + in_trainset ? + scratchpad.trainset.track : + vehicle.track_name ) }; + auto const offset { vehicle.offset }; + auto const drivertype { vehicle.driver_type }; + auto const couplingdata { ( + in_trainset ? + ( vehicle.coupling_raw.empty() ? std::to_string( vehicle.coupling ) : vehicle.coupling_raw ) : + "3" ) }; + auto const velocity { ( + in_trainset ? + scratchpad.trainset.velocity : + vehicle.velocity ) }; + + auto const couplingdatawithparams { couplingdata.find( '.' ) }; + auto coupling { ( + couplingdatawithparams != std::string::npos ? + std::atoi( couplingdata.substr( 0, couplingdatawithparams ).c_str() ) : + std::atoi( couplingdata.c_str() ) ) }; + if( coupling < 0 ) { + coupling = ( -coupling ) | coupling::permanent; + } + if( ( offset != -1.0 ) && ( std::abs( offset ) > 0.5 ) ) { + coupling = coupling::faux; + } + auto const params { ( + couplingdatawithparams != std::string::npos ? + couplingdata.substr( couplingdatawithparams + 1 ) : + "" ) }; + + auto loadcount { vehicle.load_count }; + auto loadtype { vehicle.load_type }; + + auto *path { simulation::Paths.find( pathname ) }; + if( path == nullptr ) { + ErrorLog( "Bad EU7: vehicle \"" + nodedata.name + "\" on missing track \"" + pathname + "\"" ); + return; + } + + if( ( true == scratchpad.trainset.vehicles.empty() ) + && ( false == path->m_events0.empty() ) + && ( std::abs( velocity ) <= 1.f ) + && ( scratchpad.trainset.offset >= 0.0 ) + && ( scratchpad.trainset.offset < 8.0 ) ) { + scratchpad.trainset.offset = 8.0f; + } + + auto *dyn { new TDynamicObject() }; + auto const length { dyn->Init( + nodedata.name, + datafolder, skinfile, mmdfile, + path, + ( offset == -1.0 ? + scratchpad.trainset.offset : + scratchpad.trainset.offset - static_cast( offset ) ), + drivertype, + velocity, + scratchpad.trainset.name, + loadcount, loadtype, + ( offset == -1.0 ), + params ) }; + + if( length != 0.0 ) { + scratchpad.trainset.offset -= static_cast( length ); + if( ( coupling != 0 ) + && ( dyn->MoverParameters->Couplers[ ( offset == -1.0 ? end::front : end::rear ) ].AllowedFlag & coupling::permanent ) ) { + coupling |= coupling::permanent; + } + if( in_trainset ) { + scratchpad.trainset.vehicles.emplace_back( dyn ); + scratchpad.trainset.couplings.emplace_back( coupling ); + } + } + else { + if( dyn->MyTrack != nullptr ) { + dyn->MyTrack->RemoveDynamicObject( dyn ); + } + delete dyn; + return; + } + + if( vehicle.destination ) { + dyn->asDestination = *vehicle.destination; + } + + if( dyn->mdModel != nullptr ) { + for( auto const &smokesource : dyn->mdModel->smoke_sources() ) { + Particles.insert( smokesource.first, dyn, smokesource.second ); + } + } + + if( false == in_trainset ) { + if( false == simulation::Vehicles.insert( dyn ) ) { + ErrorLog( "Bad EU7: duplicate vehicle name \"" + dyn->name() + "\"" ); + } + if( ( dyn->MoverParameters->CategoryFlag == 1 ) + && ( ( ( dyn->LightList( end::front ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) + || ( ( dyn->LightList( end::rear ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) ) ) { + simulation::Lights.insert( dyn ); + } + } + } }; + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().dynamic_ms }; + scene::eu7::load_stats().dynamics += scene.dynamics.size(); + for( std::size_t i { 0 }; i < scene.dynamics.size(); ++i ) { + if( false == used_in_trainset[ i ] ) { + apply_dynamic( scene.dynamics[ i ], false ); + } + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().sound_ms }; + scene::eu7::load_stats().sounds += scene.sounds.size(); + for( auto const &sound : scene.sounds ) { + apply_node_transform( sound.node ); + auto const nodedata { node_data_from_eu7( sound.node ) }; + auto const location { transform( + scene::eu7::inverse_transform_point( sound.location, sound.node.transform ), + scratchpad ) }; + auto *snd { new sound_source( sound_placement::external, static_cast( nodedata.range_max ) ) }; + snd->offset( location ); + snd->name( nodedata.name ); + snd->deserialize( sound.wav_file, sound_type::single ); + if( false == simulation::Sounds.insert( snd ) ) { + ErrorLog( "Bad EU7: duplicate sound name \"" + snd->name() + "\"" ); + } + simulation::Region->insert( snd ); + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().event_ms }; + scene::eu7::load_stats().events += scene.events.size(); + for( auto const &event : scene.events ) { + auto *ev { make_event_from_eu7( event ) }; + if( ev == nullptr ) { + continue; + } + + std::ostringstream body; + body << event.delay << ' ' << join_event_targets( event.targets ) << ' '; + for( auto const &[key, value] : event.payload ) { + if( false == key.empty() ) { + body << key << ' '; + } + if( false == value.empty() ) { + body << value << ' '; + } + } + if( event.delay_random != 0.0 ) { + body << "randomdelay " << event.delay_random << ' '; + } + if( event.delay_departure != 0.0 ) { + body << "departuredelay " << event.delay_departure << ' '; + } + if( event.passive ) { + body << "passive "; + } + body << "endevent"; + + cParser parser( body.str(), cParser::buffer_TEXT, "", false ); + ev->deserialize( parser, scratchpad ); + + if( true == simulation::Events.insert( ev ) ) { + scene::Groups.insert( scene::Groups.handle(), ev ); + } + else { + delete ev; + } + } + } + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().first_init_ms }; + for( std::uint32_t i { 0 }; i < scene.first_init_count; ++i ) { + if( true == scratchpad.initialized ) { + continue; + } + if( true == scratchpad.binary.terrain ) { + if( false == scratchpad.binary.terrain_included ) { + if( false == scene::eu7::try_load_scenario_terrain( *Region, scratchpad.name ) ) { + Region->deserialize( scratchpad.name ); + } + } + } + simulation::Paths.InitTracks(); + simulation::Traction.InitTraction(); + simulation::Events.InitEvents(); + simulation::Events.InitLaunchers(); + simulation::Memory.InitCells(); + if( false == scratchpad.time_initialized ) { + init_time(); + } + scratchpad.initialized = true; + } + } + + cParser dummy_parser( "", cParser::buffer_TEXT, "", false ); + + auto const load_eu7_trainset { [&]( + scene::eu7::Eu7Trainset const &trainset, + std::vector const &vehicles ) { + if( true == scratchpad.trainset.is_open ) { + deserialize_endtrainset( dummy_parser, scratchpad ); + ErrorLog( "Bad EU7: nested trainset definitions" ); + } + + scratchpad.trainset = scene::scratch_data::trainset_data(); + scratchpad.trainset.is_open = true; + scratchpad.trainset.name = trainset.name; + scratchpad.trainset.track = trainset.track; + scratchpad.trainset.offset = trainset.offset; + scratchpad.trainset.velocity = trainset.velocity; + scratchpad.trainset.assignment = trainset.assignment; + + std::size_t vehicle_slot { 0 }; + for( auto const &vehicle_source : vehicles ) { + auto vehicle { vehicle_source }; + if( vehicle_slot < trainset.couplings.size() ) { + vehicle.coupling_raw = std::to_string( trainset.couplings[ vehicle_slot ] ); + } + apply_dynamic( vehicle, true ); + ++vehicle_slot; + } + + deserialize_endtrainset( dummy_parser, scratchpad ); + } }; + + { + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().trainset_ms }; + std::size_t loaded_now { 0 }; + for( auto const &trainset : scene.trainsets ) { + if( eu7_should_load_trainset_now( trainset, scene ) ) { + std::vector vehicles; + vehicles.reserve( trainset.vehicle_indices.size() ); + for( auto const index : trainset.vehicle_indices ) { + if( index < scene.dynamics.size() ) { + vehicles.push_back( scene.dynamics[ index ] ); + } + } + load_eu7_trainset( trainset, vehicles ); + ++loaded_now; + } + else { + eu7_queue_deferred_trainset( trainset, scene ); + } + } + scene::eu7::load_stats().trainsets += loaded_now; + if( false == g_deferred_eu7_trainsets.empty() ) { + WriteLog( + "EU7: odlozono " + std::to_string( g_deferred_eu7_trainsets.size() ) + + " skladow poza " + std::to_string( static_cast( kDeferTrainsetHorizDistM ) ) + + "m (ladowanie w tle)" ); + } + } +} + +void +state_serializer::drain_deferred_eu7_trainsets( double const max_ms ) { + if( g_deferred_eu7_trainsets.empty() ) { + return; + } + + auto const deadline { + std::chrono::steady_clock::now() + + std::chrono::duration_cast( + std::chrono::duration( max_ms ) ) }; + + cParser dummy_parser( "", cParser::buffer_TEXT, "", false ); + scene::scratch_data scratchpad; + + while( + false == g_deferred_eu7_trainsets.empty() && + std::chrono::steady_clock::now() < deadline ) { + auto job { std::move( g_deferred_eu7_trainsets.front() ) }; + g_deferred_eu7_trainsets.pop_front(); + + scene::eu7::ScopedTimer const timer { scene::eu7::load_stats().trainset_ms }; + ++scene::eu7::load_stats().trainsets; + + if( true == scratchpad.trainset.is_open ) { + deserialize_endtrainset( dummy_parser, scratchpad ); + scratchpad.trainset.is_open = false; + } + + scratchpad.trainset = scene::scratch_data::trainset_data(); + scratchpad.trainset.is_open = true; + scratchpad.trainset.name = job.trainset.name; + scratchpad.trainset.track = job.trainset.track; + scratchpad.trainset.offset = job.trainset.offset; + scratchpad.trainset.velocity = job.trainset.velocity; + scratchpad.trainset.assignment = job.trainset.assignment; + + std::size_t vehicle_slot { 0 }; + for( auto const &vehicle_source : job.vehicles ) { + auto vehicle { vehicle_source }; + if( vehicle_slot < job.trainset.couplings.size() ) { + vehicle.coupling_raw = std::to_string( job.trainset.couplings[ vehicle_slot ] ); + } + + auto datafolder { vehicle.data_folder }; + auto skinfile { vehicle.skin_file }; + auto mmdfile { vehicle.mmd_file }; + replace_slashes( datafolder ); + replace_slashes( skinfile ); + replace_slashes( mmdfile ); + + auto const pathname { scratchpad.trainset.track }; + auto const offset { vehicle.offset }; + auto const drivertype { vehicle.driver_type }; + auto const couplingdata { ( + vehicle.coupling_raw.empty() ? + std::to_string( vehicle.coupling ) : + vehicle.coupling_raw ) }; + auto const velocity { scratchpad.trainset.velocity }; + + auto const couplingdatawithparams { couplingdata.find( '.' ) }; + auto coupling { ( + couplingdatawithparams != std::string::npos ? + std::atoi( couplingdata.substr( 0, couplingdatawithparams ).c_str() ) : + std::atoi( couplingdata.c_str() ) ) }; + if( coupling < 0 ) { + coupling = ( -coupling ) | coupling::permanent; + } + if( ( offset != -1.0 ) && ( std::abs( offset ) > 0.5 ) ) { + coupling = coupling::faux; + } + auto const params { ( + couplingdatawithparams != std::string::npos ? + couplingdata.substr( couplingdatawithparams + 1 ) : + "" ) }; + + auto loadcount { vehicle.load_count }; + auto loadtype { vehicle.load_type }; + + auto *path { simulation::Paths.find( pathname ) }; + if( path == nullptr ) { + ++vehicle_slot; + continue; + } + + if( + scratchpad.trainset.vehicles.empty() && + false == path->m_events0.empty() && + std::abs( velocity ) <= 1.f && + scratchpad.trainset.offset >= 0.0 && + scratchpad.trainset.offset < 8.0 ) { + scratchpad.trainset.offset = 8.0f; + } + + auto const nodedata { node_data_from_eu7( vehicle.node ) }; + auto *dyn { new TDynamicObject() }; + auto const length { dyn->Init( + nodedata.name, + datafolder, skinfile, mmdfile, + path, + ( offset == -1.0 ? + scratchpad.trainset.offset : + scratchpad.trainset.offset - static_cast( offset ) ), + drivertype, + velocity, + scratchpad.trainset.name, + loadcount, loadtype, + ( offset == -1.0 ), + params ) }; + + if( length != 0.0 ) { + scratchpad.trainset.offset -= static_cast( length ); + if( + ( coupling != 0 ) && + ( dyn->MoverParameters->Couplers[ ( offset == -1.0 ? end::front : end::rear ) ].AllowedFlag & + coupling::permanent ) ) { + coupling |= coupling::permanent; + } + scratchpad.trainset.vehicles.emplace_back( dyn ); + scratchpad.trainset.couplings.emplace_back( coupling ); + } + else { + if( dyn->MyTrack != nullptr ) { + dyn->MyTrack->RemoveDynamicObject( dyn ); + } + delete dyn; + } + + if( vehicle.destination ) { + if( false == scratchpad.trainset.vehicles.empty() ) { + scratchpad.trainset.vehicles.back()->asDestination = *vehicle.destination; + } + } + + if( + false == scratchpad.trainset.vehicles.empty() && + scratchpad.trainset.vehicles.back()->mdModel != nullptr ) { + for( auto const &smokesource : scratchpad.trainset.vehicles.back()->mdModel->smoke_sources() ) { + Particles.insert( + smokesource.first, + scratchpad.trainset.vehicles.back(), + smokesource.second ); + } + } + + ++vehicle_slot; + } + + deserialize_endtrainset( dummy_parser, scratchpad ); + } +} + void state_serializer::deserialize_isolated( cParser &Input, scene::scratch_data &Scratchpad ) { // first parameter specifies name of parent piece... @@ -366,7 +1776,9 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc // TBD: postpone loading furter and only load required blocks during the simulation? if (false == Scratchpad.binary.terrain_included) { - Region->deserialize(Scratchpad.name); + if( false == scene::eu7::try_load_scenario_terrain( *Region, Scratchpad.name ) ) { + Region->deserialize( Scratchpad.name ); + } } } @@ -788,15 +2200,34 @@ state_serializer::deserialize_terrain(cParser &Input, scene::scratch_data &Scrat std::string line; Input.getTokens(1); Input >> line; - if (Global.file_binary_terrain && line.ends_with(".sbt")) - { - Scratchpad.binary.terrain = Region->is_scene(line); - Global.file_binary_terrain_state = true; - Scratchpad.binary.terrain_included = true; - Scratchpad.terrain_name = line; - WriteLog("Included SBT file: " + line); - Region->deserialize(Scratchpad.terrain_name); - + if ( Global.file_binary_terrain + && ( line.ends_with( ".sbt" ) || line.ends_with( ".eu7" ) ) ) + { + auto const eu7path { scene::eu7::terrain_binary_path( line ) }; + if( scene::eu7::probe_terrain_file( eu7path ) ) { + Scratchpad.binary.terrain = true; + Global.file_binary_terrain_state = true; + Scratchpad.binary.terrain_included = true; + Scratchpad.terrain_name = line; + WriteLog( "Included EU7 terrain: " + eu7path ); + scene::eu7::load_terrain( *Region, eu7path ); + } + else if( scene::eu7::probe_file( eu7path ) ) { + Scratchpad.binary.terrain = true; + Global.file_binary_terrain_state = true; + Scratchpad.binary.terrain_included = true; + Scratchpad.terrain_name = line; + WriteLog( "Included EU7 module (SBT skipped): " + eu7path ); + scene::eu7::load_module( eu7path, *this ); + } + else if( Region->is_scene( line ) ) { + Scratchpad.binary.terrain = true; + Global.file_binary_terrain_state = true; + Scratchpad.binary.terrain_included = true; + Scratchpad.terrain_name = line; + WriteLog( "Included SBT file: " + line ); + Region->deserialize( Scratchpad.terrain_name ); + } } skip_until(Input, "endterrain"); @@ -856,6 +2287,18 @@ state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data & light::redmarker_left | light::redmarker_right | light::rearendsignals : light::rearendsignals ) ); } + + for( auto *vehicle : Scratchpad.trainset.vehicles ) { + if( false == simulation::Vehicles.insert( vehicle ) ) { + ErrorLog( "Bad trainset: duplicate vehicle name \"" + vehicle->name() + "\"" ); + } + if( ( vehicle->MoverParameters->CategoryFlag == 1 ) + && ( ( ( vehicle->LightList( end::front ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) + || ( ( vehicle->LightList( end::rear ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) ) ) { + simulation::Lights.insert( vehicle ); + } + } + // all done Scratchpad.trainset.is_open = false; } diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index dea1e20e..7d7c1c06 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -9,11 +9,21 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include + #include "utilities/parser.h" +#include "scene/eu7/eu7_types.h" #include "scene/scene.h" +class TModel3d; + namespace simulation { +struct eu7_pack_apply_session { + std::unordered_map *mesh_cache { nullptr }; + std::unordered_map *nodedata_cache { nullptr }; +}; + struct deserializer_state { std::string scenariofile; cParser input; @@ -45,8 +55,89 @@ public: TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position); // create new eventlauncher from node stirng TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position); + // parsuje wyemitowany tekst modulu EU7B przez istniejacy dispatch tokenow + void + deserialize_module_text( + std::string const &Text, + std::string const &SourceName, + scene::scratch_data &Scratchpad ); + // Fallback SCM/INC gdy brak child .eu7 — normalny parser z include. + void + deserialize_include_file( + std::string const &IncludeReference, + std::string const &CurrentRelativeFile, + std::vector const &Parameters, + scene::scratch_data &Scratchpad ); + // Bezposrednio z Eu7Scene — bez emit/tokenow. + void + apply_eu7_scene( + scene::eu7::Eu7Scene const &Scene, + scene::scratch_data &Scratchpad ); + // Tylko MODL z Eu7Scene — bez TRAK/EVNT/… + void + apply_eu7_models( + std::vector const &Models, + scene::scratch_data &Scratchpad ); + // MODL z chunku PACK (v7) — location/angles juz w world-space. + void + apply_eu7_pack_models( + std::vector const &Models, + scene::scratch_data &Scratchpad ); + void + apply_eu7_pack_models( + std::vector const &Models, + scene::scratch_data &Scratchpad, + std::size_t Offset, + std::size_t Count, + eu7_pack_apply_session const *Session = nullptr ); + // Wskaznik do bufora zdekodowanego przez worker — bez kopiowania wektora. + void + apply_eu7_pack_models( + scene::eu7::Eu7Model const *Models, + std::size_t Count, + scene::scratch_data &Scratchpad, + eu7_pack_apply_session const *Session = nullptr ); + // Chunk insert: Models + Offset w buforze workera (pending_apply_offset). + void + apply_eu7_pack_models( + scene::eu7::Eu7Model const *Models, + std::size_t Offset, + std::size_t Count, + scene::scratch_data &Scratchpad, + eu7_pack_apply_session const *Session = nullptr ); + // Odlozone sklady AI — ladowane w tle po wejsciu w gre. + void + drain_deferred_eu7_trainsets( double MaxMs = 12.0 ); private: + struct eu7_transform_state; + void + insert_eu7_models( + std::vector const &Models, + scene::scratch_data &Scratchpad, + eu7_transform_state &State ); + void + insert_eu7_pack_models( + std::vector const &Models, + scene::scratch_data &Scratchpad ); + void + insert_eu7_pack_models( + std::vector const &Models, + scene::scratch_data &Scratchpad, + std::size_t Offset, + std::size_t Count, + eu7_pack_apply_session const *Session = nullptr ); + void + insert_eu7_pack_models( + scene::eu7::Eu7Model const *Models, + std::size_t Count, + scene::scratch_data &Scratchpad, + eu7_pack_apply_session const *Session = nullptr ); + void + deserialize_parser_tokens( + cParser &Input, + scene::scratch_data &Scratchpad, + std::string const &SourceName ); // methods // restores class data from provided stream void deserialize_area( cParser &Input, scene::scratch_data &Scratchpad ); @@ -88,6 +179,10 @@ private: // transforms provided location by specifed rotation and offset glm::dvec3 transform( glm::dvec3 Location, scene::scratch_data const &Scratchpad ); void export_nodes_to_stream( std::ostream &, bool Dirty ) const; + void populate_deserialize_functionmap( + std::unordered_map &Functionmap, + cParser &Input, + scene::scratch_data &Scratchpad ); }; } // simulation diff --git a/tools/parse_minidump.py b/tools/parse_minidump.py new file mode 100644 index 00000000..43e3ec80 --- /dev/null +++ b/tools/parse_minidump.py @@ -0,0 +1,174 @@ + import struct +import sys +from pathlib import Path + +STREAM_NAMES = { + 0: "Unused", + 3: "ThreadList", + 4: "ModuleList", + 5: "MemoryList", + 6: "Exception", + 7: "SystemInfo", + 9: "Memory64List", + 12: "HandleData", + 14: "UnloadedModule", + 15: "MiscInfo", + 16: "MemoryInfoList", + 17: "ThreadInfoList", +} + +EXCEPTION_NAMES = { + 0x80000003: "STATUS_BREAKPOINT", + 0xC0000005: "STATUS_ACCESS_VIOLATION", + 0xC000001D: "STATUS_ILLEGAL_INSTRUCTION", + 0xC0000094: "STATUS_INTEGER_DIVIDE_BY_ZERO", + 0xC00000FD: "STATUS_STACK_OVERFLOW", + 0xE06D7363: "MSVC_CPP_EXCEPTION", +} + + +def read_cstring(f, rva: int) -> str: + pos = f.tell() + f.seek(rva) + data = bytearray() + while True: + b = f.read(1) + if not b or b == b"\x00": + break + data.extend(b) + f.seek(pos) + return data.decode("utf-8", "replace") + + +def parse(path: Path) -> None: + with path.open("rb") as f: + sig, ver, num_streams, dir_rva, checksum, ts, flags = struct.unpack( + "= 2: + op = {0: "read", 1: "write", 8: "DEP"}.get(params[0], str(params[0])) + print(f"Access: {op} at 0x{params[1]:016X}") + + mod_streams = [s for s in streams if s[0] == 4] + if mod_streams: + _, _, mod_rva = mod_streams[0] + f.seek(mod_rva) + num_modules = struct.unpack("= 1232: + f.seek(tctx_rva) + ctx = f.read(tctx_size) + # CONTEXT_AMD64 offsets + rip = struct.unpack(" 1 + else r"C:\Program Files (x86)\Steam\steamapps\common\MaSzyna\crash_2026-06-11_02-31-05.dmp" + ) + parse(dump) diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 0dba2041..b7792a5c 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -638,6 +638,24 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string& return true; } + if (token == "eu7.pack.stream.workers.percent") + { + ParseOneClamped(Parser, eu7_pack_stream_workers_percent, 1, 100); + return true; + } + + if (token == "eu7.auto.bake") + { + ParseOne(Parser, eu7_auto_bake, 1, false); + return true; + } + + if (token == "eu7.bake.threads") + { + ParseOneClamped(Parser, eu7_bake_threads, 0, 64); + return true; + } + if (token == "inactivepause") { ParseOne(Parser, bInactivePause); @@ -1588,6 +1606,9 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "latitude", fLatitudeDeg ); export_as_text( Output, "convertmodels", iConvertModels ); export_as_text( Output, "file.binary.terrain", file_binary_terrain ); + export_as_text( Output, "eu7.pack.stream.workers.percent", eu7_pack_stream_workers_percent ); + export_as_text( Output, "eu7.auto.bake", eu7_auto_bake ); + export_as_text( Output, "eu7.bake.threads", eu7_bake_threads ); export_as_text( Output, "inactivepause", bInactivePause ); export_as_text( Output, "slowmotion", iSlowMotionMask ); export_as_text( Output, "hideconsole", bHideConsole ); diff --git a/utilities/Globals.h b/utilities/Globals.h index a21a191e..bd0e2fbd 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -82,6 +82,9 @@ struct global_settings { int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan bool file_binary_terrain{ true }; // enable binary terrain (de)serialization bool file_binary_terrain_state{true}; + int eu7_pack_stream_workers_percent{ 40 }; // EU7 PACK async loader thread pool size (% of CPU threads) + bool eu7_auto_bake{ true }; // przy ladowaniu mapy: bake SCM→EU7 gdy brak lub nieaktualny .eu7 + int eu7_bake_threads{ 0 }; // watki bake (0 = auto = hardware_concurrency) // logs bool priorityLoadText3D{false}; // ladowanie T3D priorytetowo int iWriteLogEnabled{ 3 }; // maska bitowa: 1-zapis do pliku, 2-okienko, 4-nazwy torów diff --git a/utilities/parser.cpp b/utilities/parser.cpp index 68656c5d..237ae191 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -12,6 +12,18 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "scene/scenenodegroups.h" +#include "scene/eu7/eu7_loader.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +namespace scene { +class basic_region; +} +namespace simulation { +class state_manager; +extern scene::basic_region *Region; +extern state_manager State; +} /* MaSzyna EU07 locomotive simulator parser @@ -326,9 +338,39 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri return; } - const bool isTerrain = contains(includefile, "_ter.scm"); + const bool isTerrain = + contains( includefile, "_ter.scm" ) || contains( includefile, "_ter.eu7" ) || + includefile.ends_with( ".eu7" ); + auto const eu7path { + scene::eu7::include_eu7_path( mPath, mFile, includefile ) }; + auto const resolved_include { + scene::eu7::resolve_parser_include_path( mPath, mFile, includefile ) }; + if ( scene::eu7::is_module_loaded( eu7path ) ) { + readParameters( srcParser ); + return; + } + if ( scene::eu7::should_use_binary_module( resolved_include ) && simulation::Region != nullptr ) { + if ( isTerrain && true == Global.file_binary_terrain ) { + if ( scene::eu7::probe_terrain_file( eu7path ) ) { + if ( scene::eu7::load_terrain( *simulation::Region, eu7path ) ) { + Global.file_binary_terrain_state = true; + WriteLog( "EU7 terrain loaded, ignoring: " + includefile ); + readParameters( srcParser ); + return; + } + } + } + else if ( simulation::State.load_eu7_module( eu7path ) ) { + WriteLog( "EU7 module loaded, ignoring: " + includefile ); + readParameters( srcParser ); + return; + } + } + else if ( scene::eu7::probe_file( eu7path ) ) { + WriteLog( "EU7: przestarzaly .eu7, fallback SCM: " + includefile ); + } if (isTerrain && true == Global.file_binary_terrain_state) { - WriteLog("SBT found, ignoring: " + includefile); + WriteLog("Binary terrain present, ignoring: " + includefile); readParameters(srcParser); // preserve original side-effect: still consume parameters return; } diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 504078c6..4f4843dc 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -62,12 +62,30 @@ GetSubmodelFromName( TModel3d * const Model, std::string const Name ) { std::string TextureTest( std::string const &Name ) { - auto const lookup { - FileExists( - { Global.asCurrentTexturePath + Name, Name, paths::textures + Name }, - { ".mat", ".dds", ".tga", ".ktx", ".png", ".bmp", ".jpg", ".tex" } ) }; + static std::vector const extensions { + ".mat", ".dds", ".tga", ".ktx", ".png", ".bmp", ".jpg", ".tex" }; - return ( lookup.first + lookup.second ); + auto const try_paths { [&]( std::vector const &names ) { + auto const lookup { FileExists( names, extensions ) }; + return lookup.first.empty() ? std::string {} : ( lookup.first + lookup.second ); + } }; + + auto const resolved { + try_paths( { Global.asCurrentTexturePath + Name, Name, paths::textures + Name } ) }; + if( false == resolved.empty() ) { + return resolved; + } + + if( Name.starts_with( "skins/" ) ) { + auto const slash { Name.find_last_of( '/' ) }; + auto const basename { + slash != std::string::npos ? Name.substr( slash + 1 ) : Name }; + return try_paths( { + "dynamic/pkp/elf_v1/" + basename, + paths::textures + Name } ); + } + + return {}; } //--------------------------------------------------------------------------- @@ -297,33 +315,51 @@ material_data::assign( std::string const &Replacableskin ) { int skinindex = 0; std::string texturename; nameparser >> texturename; while( ( texturename != "" ) && ( skinindex < 4 ) ) { - replacable_skins[ skinindex + 1 ] = GfxRenderer->Fetch_Material( texturename ); + auto const resolved { TextureTest( ToLower( texturename ) ) }; + replacable_skins[ skinindex + 1 ] = GfxRenderer->Fetch_Material( + resolved.empty() ? texturename : resolved ); ++skinindex; texturename = ""; nameparser >> texturename; } multi_textures = skinindex; } else { - // otherwise try the basic approach - int skinindex = 0; - do { - // test quietly for file existence so we don't generate tons of false errors in the log - // NOTE: this means actual missing files won't get reported which is hardly ideal, but still somewhat better - auto const material { TextureTest( ToLower( Replacableskin + "," + std::to_string( skinindex + 1 ) ) ) }; - if( true == material.empty() ) { break; } + auto const direct { TextureTest( ToLower( Replacableskin ) ) }; + if( false == direct.empty() ) { + replacable_skins[ 1 ] = GfxRenderer->Fetch_Material( direct ); + multi_textures = 1; + } + else { + int skinindex = 0; + do { + // test quietly for file existence so we don't generate tons of false errors in the log + // NOTE: this means actual missing files won't get reported which is hardly ideal, but still somewhat better + auto const material { TextureTest( ToLower( Replacableskin + "," + std::to_string( skinindex + 1 ) ) ) }; + if( true == material.empty() ) { break; } - replacable_skins[ skinindex + 1 ] = GfxRenderer->Fetch_Material( material ); - ++skinindex; - } while( skinindex < 4 ); - multi_textures = skinindex; - if( multi_textures == 0 ) { - // zestaw nie zadziałał, próbujemy normanie - replacable_skins[ 1 ] = GfxRenderer->Fetch_Material( Replacableskin ); + replacable_skins[ skinindex + 1 ] = GfxRenderer->Fetch_Material( material ); + ++skinindex; + } while( skinindex < 4 ); + multi_textures = skinindex; + if( multi_textures == 0 ) { + auto const resolved { TextureTest( ToLower( Replacableskin ) ) }; + if( false == resolved.empty() ) { + replacable_skins[ 1 ] = GfxRenderer->Fetch_Material( resolved ); + multi_textures = 1; + } + } } } if( replacable_skins[ 1 ] == null_handle ) { - // last ditch attempt, check for single replacable skin texture - replacable_skins[ 1 ] = GfxRenderer->Fetch_Material( Replacableskin ); + auto const resolved { TextureTest( ToLower( Replacableskin ) ) }; + if( false == resolved.empty() ) { + replacable_skins[ 1 ] = GfxRenderer->Fetch_Material( resolved ); + } + } + + if( replacable_skins[ 1 ] == null_handle ) { + textures_alpha = 0x30300030; + return; } // BUGS! it's not entierly designed whether opacity is property of material or submodel, diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index aa509f1b..84b6371c 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -185,6 +185,10 @@ struct material_data { void assign( std::string const &Replacableskin ); }; +// resolves texture/material path (textures/, current path, as-is) +std::string +TextureTest( std::string const &Name ); + class TDynamicObject { // klasa pojazdu friend opengl_renderer; diff --git a/world/EvLaunch.cpp b/world/EvLaunch.cpp index eebc4255..cd0dab64 100644 --- a/world/EvLaunch.cpp +++ b/world/EvLaunch.cpp @@ -86,7 +86,8 @@ bool TEventLauncher::Load(cParser *parser) parser->getTokens(); *parser >> token; asEvent2Name = token; // drugi event - if ((asEvent2Name == "end") || (asEvent2Name == "condition") || (asEvent2Name == "traintriggered")) + if ((asEvent2Name == "end") || (asEvent2Name == "endeventlauncher") || + (asEvent2Name == "condition") || (asEvent2Name == "traintriggered")) { // drugiego eventu może nie być, bo są z tym problemy, ale ciii... token = asEvent2Name; // rozpoznane słowo idzie do dalszego przetwarzania asEvent2Name = "none"; // a drugiego eventu nie ma diff --git a/world/Event.cpp b/world/Event.cpp index fad60bc4..a5163126 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -2235,6 +2235,84 @@ make_event( cParser &Input, scene::scratch_data &Scratchpad ) { return event; } +basic_event * +make_event_from_eu7( scene::eu7::Eu7Event const &event ) { + + std::string const name { ToLower( ( + event.ignored ? + std::string{ "none_" } + event.name : + event.name ) ) }; + std::string type; + + switch( event.type ) { + case scene::eu7::Eu7EventType::AddValues: + type = "addvalues"; + break; + case scene::eu7::Eu7EventType::UpdateValues: + type = "updatevalues"; + break; + case scene::eu7::Eu7EventType::CopyValues: + type = "copyvalues"; + break; + case scene::eu7::Eu7EventType::GetValues: + type = "getvalues"; + break; + case scene::eu7::Eu7EventType::PutValues: + type = "putvalues"; + break; + case scene::eu7::Eu7EventType::Whois: + type = "whois"; + break; + case scene::eu7::Eu7EventType::LogValues: + type = "logvalues"; + break; + case scene::eu7::Eu7EventType::Multiple: + type = "multiple"; + break; + case scene::eu7::Eu7EventType::Switch: + type = "switch"; + break; + case scene::eu7::Eu7EventType::TrackVel: + type = "trackvel"; + break; + case scene::eu7::Eu7EventType::Sound: + type = "sound"; + break; + case scene::eu7::Eu7EventType::Texture: + type = "texture"; + break; + case scene::eu7::Eu7EventType::Animation: + type = "animation"; + break; + case scene::eu7::Eu7EventType::Lights: + type = "lights"; + break; + case scene::eu7::Eu7EventType::Voltage: + type = "voltage"; + break; + case scene::eu7::Eu7EventType::Visible: + type = "visible"; + break; + case scene::eu7::Eu7EventType::Friction: + type = "friction"; + break; + case scene::eu7::Eu7EventType::Message: + type = "message"; + break; + default: + ErrorLog( "Bad EU7 event: unrecognized type for event \"" + name + "\"" ); + return nullptr; + } + + cParser parser( name + " " + type, cParser::buffer_TEXT, "", false ); + scene::scratch_data scratch; + auto *result { make_event( parser, scratch ) }; + if( result != nullptr && event.passive ) { + result->m_passive = true; + } + return result; +} + //--------------------------------------------------------------------------- event_manager::~event_manager() { diff --git a/world/Event.h b/world/Event.h index d04b4c87..e898b660 100644 --- a/world/Event.h +++ b/world/Event.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "utilities/Classes.h" +#include "scene/eu7/eu7_types.h" #include "scene/scene.h" #include "utilities/Names.h" #include "world/EvLaunch.h" @@ -633,6 +634,9 @@ private: basic_event * make_event( cParser &Input, scene::scratch_data &Scratchpad ); +basic_event * +make_event_from_eu7( scene::eu7::Eu7Event const &Event ); + class event_manager { diff --git a/world/Track.cpp b/world/Track.cpp index 07a3c01d..7b6682f9 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -15,6 +15,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/Track.h" + +#include "scene/eu7/eu7_types.h" #include "simulation/simulation.h" #include "utilities/Globals.h" #include "world/Event.h" @@ -532,6 +534,29 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } + build_segments_from_paths(); + + parser->getTokens(); + *parser >> token; + str = token; + while( str != "endtrack" ) { + parser->getTokens(); + *parser >> token; + apply_tail_keyword( str, token ); + parser->getTokens(); + *parser >> token; + str = token; + } + finalize_track_location(); +} + +void +TTrack::build_segments_from_paths() { + + glm::dvec3 pt, vec, p1, p2, cp1, cp2, p3, p4, cp3, cp4; + double a1, a2, r1, r2, r3, r4; + double segsize = 5.0; + switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym case tt_Table: { @@ -807,177 +832,217 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) break; } } +} - // optional attributes - parser->getTokens(); - *parser >> token; - str = token; - while (str != "endtrack") - { - if (str == "event0") - { - parser->getTokens(); - *parser >> token; - m_events0.emplace_back( token, nullptr ); - } - else if (str == "event1") - { - parser->getTokens(); - *parser >> token; - m_events1.emplace_back( token, nullptr ); - } - else if (str == "event2") - { - parser->getTokens(); - *parser >> token; - m_events2.emplace_back( token, nullptr ); - } - else if (str == "eventall0") - { - parser->getTokens(); - *parser >> token; - m_events0all.emplace_back( token, nullptr ); - } - else if (str == "eventall1") - { - parser->getTokens(); - *parser >> token; - m_events1all.emplace_back( token, nullptr ); - } - else if (str == "eventall2") - { - parser->getTokens(); - *parser >> token; - m_events2all.emplace_back( token, nullptr ); - } - else if (str == "velocity") - { - parser->getTokens(); - *parser >> fVelocity; //*0.28; McZapkie-010602 - if (SwitchExtension) // jeśli tor ruchomy - if (std::abs(fVelocity) >= 1.0) //żeby zero nie ograniczało dożywotnio - // zapamiętanie głównego ograniczenia; a np. -40 ogranicza tylko na bok - SwitchExtension->fVelocity = static_cast(fVelocity); - } - else if (str == "isolated") - { // obwód izolowany, do którego tor należy - parser->getTokens(); - *parser >> token; - Isolated.push_back(TIsolated::Find(token)); - } - else if (str == "angle1") - { // kąt ścięcia końca od strony 1 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> a1; - //Segment->AngleSet(0, a1); - } - else if (str == "angle2") - { // kąt ścięcia końca od strony 2 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> a2; - //Segment->AngleSet(1, a2); - } - else if (str == "fouling1") - { // wskazanie modelu ukresu w kierunku 1 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> token; - // nFouling[0]= - } - else if (str == "fouling2") - { // wskazanie modelu ukresu w kierunku 2 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> token; - // nFouling[1]= - } - else if (str == "overhead") - { // informacja o stanie sieci: 0-jazda bezprądowa, >0-z opuszczonym i ograniczeniem prędkości - parser->getTokens(); - *parser >> fOverhead; - if (fOverhead > 0.0) - iAction |= 0x40; // flaga opuszczenia pantografu (tor uwzględniany w skanowaniu jako - // ograniczenie dla pantografujących) - } - else if( str == "vradius" ) { - // y-axis track radius - // NOTE: not used/implemented - parser->getTokens(); - *parser >> fVerticalRadius; - } - else if( str == "trackbed" ) { - // switch trackbed texture - auto const trackbedtexture { parser->getToken() }; - if( eType == tt_Switch ) { - SwitchExtension->m_material3 = GfxRenderer->Fetch_Material( trackbedtexture ); - } - } - else if( str == "railprofile" ) { - // rail profile - auto const railprofile { parser->getToken() }; - if( iCategoryFlag == 1 ) { - m_profile1 = fetch_track_rail_profile( railprofile ); - } - } - else if( str == "friction" ) { - // memory cell holding friction value modifiers - m_friction.first = parser->getToken(); - } - else if( str == "sleepermodel" ) { - // sleepermodel - // - frequency: meters between consecutive sleeper instances (must be > 0) - // - model: path to the .e3d sleeper model - // - skin: replacable skin path, or "none" for the model's defaults - // - offset: local-space offset applied per-instance (x=left/right, y=forward/back, z=up/down) - // - ballastZ: vertical shift applied to the auto-generated trackbed (ballast). negative pushes ballast down. - float frequency { 0.f }; - float offsetx { 0.f }, offsety { 0.f }, offsetz { 0.f }; - float ballastz { 0.f }; - parser->getTokens( 1, false ); *parser >> frequency; - auto modelpath { parser->getToken( false ) }; - auto skinpath { parser->getToken( false ) }; - parser->getTokens( 3, false ); *parser >> offsetx >> offsety >> offsetz; - parser->getTokens( 1, false ); *parser >> ballastz; +void +TTrack::apply_tail_keyword( std::string const &key, std::string const &value ) { - if( frequency <= 0.01f ) { - ErrorLog( "Bad track: invalid sleepermodel frequency (" + std::to_string( frequency ) + ") for track \"" + m_name + "\"" ); - } - else { - replace_slashes( modelpath ); - m_sleeper_enabled = true; - m_sleeper_frequency = frequency; - m_sleeper_model_name = modelpath; - m_sleeper_skin_name = skinpath; - m_sleeper_offset = glm::vec3( offsetx, offsety, offsetz ); - m_sleeper_ballast_z = ballastz; - // model and skin are resolved (and instance transforms baked) in build_sleeper_transforms, - // called after segment initialisation so the path geometry is final. - } - } - else - ErrorLog("Bad track: unknown property: \"" + str + "\" defined for track \"" + m_name + "\""); - parser->getTokens(); - *parser >> token; - str = token; + if( key == "event0" ) { + m_events0.emplace_back( value, nullptr ); } - // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru - if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) - { - Isolated.push_back(TIsolated::Find(m_name.substr(i + 1, m_name.length()))); - m_name = m_name.substr(0, i - 1); // usunięcie z nazwy + else if( key == "event1" ) { + m_events1.emplace_back( value, nullptr ); + } + else if( key == "event2" ) { + m_events2.emplace_back( value, nullptr ); + } + else if( key == "eventall0" ) { + m_events0all.emplace_back( value, nullptr ); + } + else if( key == "eventall1" ) { + m_events1all.emplace_back( value, nullptr ); + } + else if( key == "eventall2" ) { + m_events2all.emplace_back( value, nullptr ); + } + else if( key == "velocity" ) { + fVelocity = std::atof( value.c_str() ); + if( SwitchExtension && std::abs( fVelocity ) >= 1.0 ) { + SwitchExtension->fVelocity = static_cast( fVelocity ); + } + } + else if( key == "isolated" ) { + Isolated.push_back( TIsolated::Find( value ) ); + } + else if( key == "overhead" ) { + fOverhead = std::atof( value.c_str() ); + if( fOverhead > 0.0 ) { + iAction |= 0x40; + } + } + else if( key == "trackbed" ) { + if( eType == tt_Switch ) { + SwitchExtension->m_material3 = GfxRenderer->Fetch_Material( value ); + } + } + else if( key == "railprofile" ) { + if( iCategoryFlag == 1 ) { + m_profile1 = fetch_track_rail_profile( value ); + } + } + else if( key == "friction" ) { + m_friction.first = value; + } + else if( key == "sleepermodel" ) { + cParser sleeperparser( value ); + float frequency { 0.f }; + float offsetx { 0.f }, offsety { 0.f }, offsetz { 0.f }; + float ballastz { 0.f }; + sleeperparser.getTokens( 1, false ); + sleeperparser >> frequency; + auto const modelpath { sleeperparser.getToken( false ) }; + auto const skinpath { sleeperparser.getToken( false ) }; + sleeperparser.getTokens( 3, false ); + sleeperparser >> offsetx >> offsety >> offsetz; + sleeperparser.getTokens( 1, false ); + sleeperparser >> ballastz; + if( frequency <= 0.01f ) { + ErrorLog( + "Bad track: invalid sleepermodel frequency (" + std::to_string( frequency ) + + ") for track \"" + m_name + "\"" ); + } + else { + auto resolved_model { modelpath }; + replace_slashes( resolved_model ); + m_sleeper_enabled = true; + m_sleeper_frequency = frequency; + m_sleeper_model_name = resolved_model; + m_sleeper_skin_name = skinpath; + m_sleeper_offset = glm::vec3( offsetx, offsety, offsetz ); + m_sleeper_ballast_z = ballastz; + } + } + else if( key != "angle1" && key != "angle2" && key != "fouling1" && key != "fouling2" && key != "vradius" ) { + ErrorLog( "Bad track: unknown property: \"" + key + "\" defined for track \"" + m_name + "\"" ); + } +} + +void +TTrack::finalize_track_location() { + + std::size_t const at_sign { m_name.find( '@' ) }; + if( at_sign != std::string::npos && at_sign < m_name.length() ) { + Isolated.push_back( TIsolated::Find( m_name.substr( at_sign + 1, m_name.length() ) ) ); + m_name = m_name.substr( 0, at_sign - 1 ); } - // calculate path location location( ( CurrentSegment()->FastGetPoint_0() + CurrentSegment()->FastGetPoint( 0.5 ) + CurrentSegment()->FastGetPoint_1() ) / 3.0 ); - // sleeper transforms are baked later in create_geometry(), once the owning cell has - // assigned this track its m_origin (otherwise the local-space matrices would be relative - // to a stale origin and the renderer would draw sleepers in the wrong place). +} + +void +TTrack::LoadFromEu7( scene::eu7::Eu7Track const &track ) { + + switch( track.track_type ) { + case scene::eu7::Eu7TrackType::Switch: + eType = tt_Switch; + iCategoryFlag = 1; + break; + case scene::eu7::Eu7TrackType::Table: + eType = tt_Table; + iCategoryFlag = 1; + break; + case scene::eu7::Eu7TrackType::Cross: + eType = tt_Cross; + iCategoryFlag = static_cast( scene::eu7::Eu7TrackCategory::Road ); + break; + case scene::eu7::Eu7TrackType::Tributary: + eType = tt_Tributary; + iCategoryFlag = static_cast( scene::eu7::Eu7TrackCategory::Water ); + break; + default: + eType = tt_Normal; + iCategoryFlag = static_cast( track.category ); + break; + } + + fTrackWidth = track.track_width; + fFriction = track.friction; + fSoundDistance = track.sound_distance; + if( eType == tt_Switch ) { + fSoundDistance = 10.0f; + } + fTrackWidth2 = fTrackWidth; + iQualityFlag = track.quality_flag; + iDamageFlag = track.damage_flag; + if( iDamageFlag & 128 ) { + iAction |= 0x80; + } + + switch( track.environment ) { + case scene::eu7::Eu7TrackEnvironment::Flat: + eEnvironment = e_flat; + break; + case scene::eu7::Eu7TrackEnvironment::Mountains: + eEnvironment = e_mountains; + break; + case scene::eu7::Eu7TrackEnvironment::Canyon: + eEnvironment = e_canyon; + break; + case scene::eu7::Eu7TrackEnvironment::Tunnel: + eEnvironment = e_tunnel; + break; + case scene::eu7::Eu7TrackEnvironment::Bridge: + eEnvironment = e_bridge; + break; + case scene::eu7::Eu7TrackEnvironment::Bank: + eEnvironment = e_bank; + break; + default: + eEnvironment = e_unknown; + break; + } + + m_visible = track.node.visible; + if( track.visibility ) { + auto mat1 { track.visibility->material1 }; + replace_slashes( mat1 ); + m_material1 = ( + mat1 == "none" ? + null_handle : + GfxRenderer->Fetch_Material( mat1 ) ); + fTexLength = track.visibility->tex_length; + if( fTexLength < 0.01f ) { + fTexLength = 4.f; + } + auto mat2 { track.visibility->material2 }; + replace_slashes( mat2 ); + m_material2 = ( + mat2 == "none" ? + null_handle : + GfxRenderer->Fetch_Material( mat2 ) ); + fTexHeight1 = track.visibility->tex_height1; + fTexWidth = track.visibility->tex_width; + fTexSlope = track.visibility->tex_slope; + if( iCategoryFlag & 4 ) { + fTexHeight1 = -fTexHeight1; + } + } + + Init(); + m_paths.clear(); + m_paths.reserve( track.paths.size() ); + for( auto const &source : track.paths ) { + segment_data seg; + seg.points[ segment_data::point::start ] = source.p_start; + seg.rolls[ 0 ] = static_cast( source.roll_start ); + seg.points[ segment_data::point::control1 ] = source.cp_out; + seg.points[ segment_data::point::control2 ] = source.cp_in; + seg.points[ segment_data::point::end ] = source.p_end; + seg.rolls[ 1 ] = static_cast( source.roll_end ); + seg.radius = source.radius; + m_paths.push_back( seg ); + } + + build_segments_from_paths(); + + for( auto const &[key, value] : track.tail_keywords ) { + apply_tail_keyword( key, value ); + } + finalize_track_location(); } bool TTrack::AssignEvents() { diff --git a/world/Track.h b/world/Track.h index 6c544203..67777557 100644 --- a/world/Track.h +++ b/world/Track.h @@ -24,6 +24,9 @@ http://mozilla.org/MPL/2.0/. namespace scene { class basic_cell; +namespace eu7 { +struct Eu7Track; +} } enum TTrackType { @@ -284,6 +287,10 @@ public: SwitchExtension->iRoads - 1 : 1 ); } void Load(cParser *parser, glm::dvec3 const &pOrigin); + void LoadFromEu7( scene::eu7::Eu7Track const &track ); + void build_segments_from_paths(); + void apply_tail_keyword( std::string const &key, std::string const &value ); + void finalize_track_location(); bool AssignEvents(); bool AssignForcedEvents(basic_event *NewEventPlus, basic_event *NewEventMinus); void QueueEvents( event_sequence const &Events, TDynamicObject const *Owner );