/* 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