mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-22 13:59:19 +02:00
Add headless parallel eu7v2 scenario bake with streaming and PLCE placements
Enable --eu7v2-bake from the main binary: parallel module pool, bounded-RAM spool flush, streaming terrain triangles, flat include/model parsing, and eu7v2 emit/load with optional verify. Large placement .scm files emit lean PLCE records and bake referenced .inc modules separately for reuse. - CLI: --eu7v2-bake, --eu7v2-verify, --eu7v2-mem-limit-gb, --eu7v2-threads, --eu7v2-max-parse; wire max_threads through to the bake parser - eu7v2 v2 records: PLCE placements, runtime emitter/loader, batch verify - Parallel bake pool with session cache; drop heavy-serial parse gate in spool mode; parse concurrency matches thread count - Streaming terrain: batched parallel parse+bake, scan/bake pipeline, shape spool with persistent buffered I/O and flush-before-read - Parallel flat-file streaming for models/includes; pack/model spool for low-memory incremental flush - Optional 50 GB private-bytes guard during headless bake Braniewo_szeroki: 160 modules, verify PASS, ~34s bake (nmt100 ~17s vs ~190s serial baseline). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
561
scene/eu7/v2/eu7v2_bake.cpp
Normal file
561
scene/eu7/v2/eu7v2_bake.cpp
Normal file
@@ -0,0 +1,561 @@
|
||||
/*
|
||||
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 "scene/eu7/v2/eu7v2_bake.h"
|
||||
|
||||
#include "scene/eu7/v2/eu7v2_format.h"
|
||||
#include "scene/eu7/v2/eu7v2_scene.h"
|
||||
#include "scene/eu7/v2/eu7v2_records.h"
|
||||
#include "scene/eu7/eu7_types.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
namespace {
|
||||
|
||||
// Bakes the parser output into the dependency-free eu7v2 structs, owning the
|
||||
// string table all chunks share.
|
||||
class scene_baker {
|
||||
public:
|
||||
explicit scene_baker( scene::eu7::Eu7Scene const &scene ) : m_scene( scene ) {}
|
||||
scene_baker( scene::eu7::Eu7Scene const &scene, scene::eu7::Eu7Module const &module )
|
||||
: m_scene( scene ), m_module( &module ) {}
|
||||
|
||||
std::vector<std::uint8_t> run() {
|
||||
convert_models();
|
||||
convert_terrain();
|
||||
convert_shapes();
|
||||
convert_lines();
|
||||
convert_tracks();
|
||||
convert_traction();
|
||||
convert_power_sources();
|
||||
convert_memcells();
|
||||
convert_launchers();
|
||||
convert_events();
|
||||
convert_sounds();
|
||||
convert_dynamics();
|
||||
convert_trainsets();
|
||||
if( m_module != nullptr ) {
|
||||
convert_includes();
|
||||
convert_meta();
|
||||
}
|
||||
return serialize();
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::uint32_t str( std::string const &s ) { return m_strings.intern( s ); }
|
||||
|
||||
[[nodiscard]] node_record node( scene::eu7::Eu7BasicNode const &n ) {
|
||||
node_record out;
|
||||
out.name = str( n.name );
|
||||
out.type = str( n.node_type );
|
||||
out.area_center = { n.area.center.x, n.area.center.y, n.area.center.z };
|
||||
out.area_radius = n.area.radius;
|
||||
out.range_sq_min = n.range_squared_min;
|
||||
out.range_sq_max = n.range_squared_max;
|
||||
out.visible = n.visible;
|
||||
return out;
|
||||
}
|
||||
|
||||
// -- models: deduplicate identical definitions into prototypes ----------
|
||||
void convert_models() {
|
||||
for( auto const &model : m_scene.models ) {
|
||||
auto const proto { intern_prototype( model ) };
|
||||
|
||||
model_instance inst;
|
||||
inst.proto = proto;
|
||||
inst.x = model.location.x;
|
||||
inst.y = model.location.y;
|
||||
inst.z = model.location.z;
|
||||
inst.ax = static_cast<float>( model.angles.x );
|
||||
inst.ay = static_cast<float>( model.angles.y );
|
||||
inst.az = static_cast<float>( model.angles.z );
|
||||
inst.sx = static_cast<float>( model.scale.x );
|
||||
inst.sy = static_cast<float>( model.scale.y );
|
||||
inst.sz = static_cast<float>( model.scale.z );
|
||||
inst.cell_id = model.pack_cell_id;
|
||||
inst.texture_override = kNoString; // within a single bake the proto already carries the skin
|
||||
inst.has_node = true;
|
||||
inst.node = node( model.node );
|
||||
m_instances.push_back( inst );
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t intern_prototype( scene::eu7::Eu7Model const &model ) {
|
||||
std::string key;
|
||||
key.reserve( model.model_file.size() + model.texture_file.size() + 32 );
|
||||
key.append( model.model_file );
|
||||
key.push_back( '\x1f' );
|
||||
key.append( model.texture_file );
|
||||
key.push_back( '\x1f' );
|
||||
key.push_back( model.transition ? '1' : '0' );
|
||||
key.push_back( model.is_terrain ? '1' : '0' );
|
||||
key.append( std::to_string( model.baked_range_min ) );
|
||||
key.push_back( '\x1f' );
|
||||
key.append( std::to_string( model.baked_range_max ) );
|
||||
key.push_back( '\x1f' );
|
||||
for( auto const s : model.light_states ) {
|
||||
key.append( std::to_string( s ) );
|
||||
key.push_back( ',' );
|
||||
}
|
||||
key.push_back( '\x1f' );
|
||||
for( auto const c : model.light_colors ) {
|
||||
key.append( std::to_string( c ) );
|
||||
key.push_back( ',' );
|
||||
}
|
||||
|
||||
auto const it { m_prototype_lookup.find( key ) };
|
||||
if( it != m_prototype_lookup.end() ) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
model_prototype proto;
|
||||
proto.model_file = str( model.model_file );
|
||||
proto.texture_file = model.texture_file.empty() ? kNoString : str( model.texture_file );
|
||||
proto.flags = 0;
|
||||
if( model.transition ) {
|
||||
proto.flags |= proto_flag::transition;
|
||||
}
|
||||
if( model.is_terrain ) {
|
||||
proto.flags |= proto_flag::is_terrain;
|
||||
}
|
||||
if( model.pack_flags & scene::eu7::kEu7PackFlagInstanceableHint ) {
|
||||
proto.flags |= proto_flag::instanceable;
|
||||
}
|
||||
proto.range_min = model.baked_range_min;
|
||||
proto.range_max = model.baked_range_max;
|
||||
proto.light_states = model.light_states;
|
||||
proto.light_colors = model.light_colors;
|
||||
|
||||
auto const id { static_cast<std::uint32_t>( m_prototypes.size() ) };
|
||||
m_prototypes.push_back( std::move( proto ) );
|
||||
m_prototype_lookup.emplace( std::move( key ), id );
|
||||
return id;
|
||||
}
|
||||
|
||||
// -- terrain: store origin in f64, vertices f32 relative to origin ------
|
||||
void convert_terrain() {
|
||||
for( auto const &shape : m_scene.terrain_shapes ) {
|
||||
terrain_mesh mesh;
|
||||
mesh.material = shape.material_path.empty() ? kNoString : str( shape.material_path );
|
||||
mesh.translucent = shape.translucent;
|
||||
mesh.ox = shape.origin.x;
|
||||
mesh.oy = shape.origin.y;
|
||||
mesh.oz = shape.origin.z;
|
||||
mesh.vertices.reserve( shape.vertices.size() );
|
||||
for( auto const &v : shape.vertices ) {
|
||||
mesh_vertex mv;
|
||||
mv.px = static_cast<float>( v.position.x - shape.origin.x );
|
||||
mv.py = static_cast<float>( v.position.y - shape.origin.y );
|
||||
mv.pz = static_cast<float>( v.position.z - shape.origin.z );
|
||||
mv.nx = v.normal.x;
|
||||
mv.ny = v.normal.y;
|
||||
mv.nz = v.normal.z;
|
||||
mv.u = static_cast<float>( v.u );
|
||||
mv.v = static_cast<float>( v.v );
|
||||
mesh.vertices.push_back( mv );
|
||||
}
|
||||
m_meshes.push_back( std::move( mesh ) );
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] lighting_block lighting( scene::eu7::Eu7LightingData const &l ) {
|
||||
lighting_block out;
|
||||
out.diffuse[ 0 ] = l.diffuse.x; out.diffuse[ 1 ] = l.diffuse.y;
|
||||
out.diffuse[ 2 ] = l.diffuse.z; out.diffuse[ 3 ] = l.diffuse.w;
|
||||
out.ambient[ 0 ] = l.ambient.x; out.ambient[ 1 ] = l.ambient.y;
|
||||
out.ambient[ 2 ] = l.ambient.z; out.ambient[ 3 ] = l.ambient.w;
|
||||
out.specular[ 0 ] = l.specular.x; out.specular[ 1 ] = l.specular.y;
|
||||
out.specular[ 2 ] = l.specular.z; out.specular[ 3 ] = l.specular.w;
|
||||
return out;
|
||||
}
|
||||
|
||||
// -- non-terrain shapes: same encoding as terrain but lossless on node ---
|
||||
void convert_shapes() {
|
||||
for( auto const &shape : m_scene.shapes ) {
|
||||
shape_record r;
|
||||
r.node = node( shape.node );
|
||||
r.translucent = shape.translucent;
|
||||
r.material = shape.material_path.empty() ? kNoString : str( shape.material_path );
|
||||
r.lighting = lighting( shape.lighting );
|
||||
r.ox = shape.origin.x;
|
||||
r.oy = shape.origin.y;
|
||||
r.oz = shape.origin.z;
|
||||
r.vertices.reserve( shape.vertices.size() );
|
||||
for( auto const &v : shape.vertices ) {
|
||||
mesh_vertex mv;
|
||||
mv.px = static_cast<float>( v.position.x - shape.origin.x );
|
||||
mv.py = static_cast<float>( v.position.y - shape.origin.y );
|
||||
mv.pz = static_cast<float>( v.position.z - shape.origin.z );
|
||||
mv.nx = v.normal.x;
|
||||
mv.ny = v.normal.y;
|
||||
mv.nz = v.normal.z;
|
||||
mv.u = static_cast<float>( v.u );
|
||||
mv.v = static_cast<float>( v.v );
|
||||
r.vertices.push_back( mv );
|
||||
}
|
||||
m_shapes.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_lines() {
|
||||
for( auto const &line : m_scene.lines ) {
|
||||
lines_record r;
|
||||
r.node = node( line.node );
|
||||
r.lighting = lighting( line.lighting );
|
||||
r.line_width = line.line_width;
|
||||
r.ox = line.origin.x;
|
||||
r.oy = line.origin.y;
|
||||
r.oz = line.origin.z;
|
||||
r.vertices.reserve( line.vertices.size() );
|
||||
for( auto const &v : line.vertices ) {
|
||||
r.vertices.push_back( { v.position.x, v.position.y, v.position.z } );
|
||||
}
|
||||
m_lines.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_tracks() {
|
||||
for( auto const &t : m_scene.tracks ) {
|
||||
track_record r;
|
||||
r.node = node( t.node );
|
||||
r.track_type = static_cast<std::uint8_t>( t.track_type );
|
||||
r.category = static_cast<std::uint8_t>( t.category );
|
||||
r.length = t.length;
|
||||
r.track_width = t.track_width;
|
||||
r.friction = t.friction;
|
||||
r.sound_distance = t.sound_distance;
|
||||
r.quality_flag = t.quality_flag;
|
||||
r.damage_flag = t.damage_flag;
|
||||
r.environment = static_cast<std::int8_t>( t.environment );
|
||||
if( t.visibility.has_value() ) {
|
||||
r.has_visibility = true;
|
||||
r.visibility.material1 = str( t.visibility->material1 );
|
||||
r.visibility.tex_length = t.visibility->tex_length;
|
||||
r.visibility.material2 = str( t.visibility->material2 );
|
||||
r.visibility.tex_height1 = t.visibility->tex_height1;
|
||||
r.visibility.tex_width = t.visibility->tex_width;
|
||||
r.visibility.tex_slope = t.visibility->tex_slope;
|
||||
}
|
||||
r.paths.reserve( t.paths.size() );
|
||||
for( auto const &p : t.paths ) {
|
||||
track_path tp;
|
||||
tp.p_start = { p.p_start.x, p.p_start.y, p.p_start.z };
|
||||
tp.roll_start = p.roll_start;
|
||||
tp.cp_out = { p.cp_out.x, p.cp_out.y, p.cp_out.z };
|
||||
tp.cp_in = { p.cp_in.x, p.cp_in.y, p.cp_in.z };
|
||||
tp.p_end = { p.p_end.x, p.p_end.y, p.p_end.z };
|
||||
tp.roll_end = p.roll_end;
|
||||
tp.radius = p.radius;
|
||||
r.paths.push_back( tp );
|
||||
}
|
||||
r.tail_keywords.reserve( t.tail_keywords.size() );
|
||||
for( auto const &kv : t.tail_keywords ) {
|
||||
r.tail_keywords.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
m_tracks.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_traction() {
|
||||
for( auto const &t : m_scene.traction ) {
|
||||
traction_record r;
|
||||
r.node = node( t.node );
|
||||
r.power_supply_name = str( t.power_supply_name );
|
||||
r.nominal_voltage = t.nominal_voltage;
|
||||
r.max_current = t.max_current;
|
||||
r.resistivity = t.resistivity_ohm_per_m;
|
||||
r.material = static_cast<std::uint8_t>( t.material );
|
||||
r.wire_thickness = t.wire_thickness;
|
||||
r.damage_flag = t.damage_flag;
|
||||
r.wire_p1 = { t.wire_p1.x, t.wire_p1.y, t.wire_p1.z };
|
||||
r.wire_p2 = { t.wire_p2.x, t.wire_p2.y, t.wire_p2.z };
|
||||
r.wire_p3 = { t.wire_p3.x, t.wire_p3.y, t.wire_p3.z };
|
||||
r.wire_p4 = { t.wire_p4.x, t.wire_p4.y, t.wire_p4.z };
|
||||
r.min_height = t.min_height;
|
||||
r.segment_length = t.segment_length;
|
||||
r.wire_count = t.wire_count;
|
||||
r.wire_offset = t.wire_offset;
|
||||
if( t.parallel_name.has_value() ) {
|
||||
r.has_parallel = true;
|
||||
r.parallel_name = str( *t.parallel_name );
|
||||
}
|
||||
m_traction.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_power_sources() {
|
||||
for( auto const &p : m_scene.power_sources ) {
|
||||
power_source_record r;
|
||||
r.node = node( p.node );
|
||||
r.position = { p.position.x, p.position.y, p.position.z };
|
||||
r.nominal_voltage = p.nominal_voltage;
|
||||
r.voltage_frequency = p.voltage_frequency;
|
||||
r.internal_resistance = p.internal_resistance;
|
||||
r.max_output_current = p.max_output_current;
|
||||
r.fast_fuse_timeout = p.fast_fuse_timeout;
|
||||
r.fast_fuse_repetition = p.fast_fuse_repetition;
|
||||
r.slow_fuse_timeout = p.slow_fuse_timeout;
|
||||
r.modifier = static_cast<std::uint8_t>( p.modifier );
|
||||
m_power.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_memcells() {
|
||||
for( auto const &m : m_scene.memcells ) {
|
||||
memcell_record r;
|
||||
r.node = node( m.node );
|
||||
r.text = str( m.text );
|
||||
r.value1 = m.value1;
|
||||
r.value2 = m.value2;
|
||||
if( m.track_name.has_value() ) {
|
||||
r.has_track = true;
|
||||
r.track_name = str( *m.track_name );
|
||||
}
|
||||
m_memcells.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_launchers() {
|
||||
for( auto const &l : m_scene.event_launchers ) {
|
||||
launcher_record r;
|
||||
r.node = node( l.node );
|
||||
r.location = { l.location.x, l.location.y, l.location.z };
|
||||
r.radius_squared = l.radius_squared;
|
||||
r.activation_key = l.activation_key;
|
||||
r.delta_time = l.delta_time;
|
||||
r.event1_name = str( l.event1_name );
|
||||
r.event2_name = str( l.event2_name );
|
||||
if( l.condition.has_value() ) {
|
||||
r.has_condition = true;
|
||||
r.condition.memcell_name = str( l.condition->memcell_name );
|
||||
r.condition.compare_text = str( l.condition->compare_text );
|
||||
r.condition.compare_value1 = l.condition->compare_value1;
|
||||
r.condition.compare_value2 = l.condition->compare_value2;
|
||||
r.condition.check_mask = l.condition->check_mask;
|
||||
}
|
||||
r.train_triggered = l.train_triggered;
|
||||
r.launch_hour = l.launch_hour;
|
||||
r.launch_minute = l.launch_minute;
|
||||
m_launchers.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_events() {
|
||||
for( auto const &e : m_scene.events ) {
|
||||
event_record r;
|
||||
r.name = str( e.name );
|
||||
r.type = static_cast<std::uint8_t>( e.type );
|
||||
r.delay = e.delay;
|
||||
r.delay_random = e.delay_random;
|
||||
r.delay_departure = e.delay_departure;
|
||||
r.ignored = e.ignored;
|
||||
r.passive = e.passive;
|
||||
r.targets.reserve( e.targets.size() );
|
||||
for( auto const &t : e.targets ) {
|
||||
r.targets.push_back( str( t ) );
|
||||
}
|
||||
r.payload.reserve( e.payload.size() );
|
||||
for( auto const &kv : e.payload ) {
|
||||
r.payload.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
m_events.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_sounds() {
|
||||
for( auto const &s : m_scene.sounds ) {
|
||||
sound_record r;
|
||||
r.node = node( s.node );
|
||||
r.location = { s.location.x, s.location.y, s.location.z };
|
||||
r.wav_file = str( s.wav_file );
|
||||
m_sounds.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_dynamics() {
|
||||
for( auto const &d : m_scene.dynamics ) {
|
||||
dynamic_record r;
|
||||
r.node = node( d.node );
|
||||
r.data_folder = str( d.data_folder );
|
||||
r.skin_file = str( d.skin_file );
|
||||
r.mmd_file = str( d.mmd_file );
|
||||
r.track_name = str( d.track_name );
|
||||
r.offset = d.offset;
|
||||
r.driver_type = str( d.driver_type );
|
||||
r.coupling = d.coupling;
|
||||
r.coupling_raw = str( d.coupling_raw );
|
||||
r.coupling_params = str( d.coupling_params );
|
||||
r.velocity = d.velocity;
|
||||
r.load_count = d.load_count;
|
||||
r.load_type = str( d.load_type );
|
||||
if( d.destination.has_value() ) {
|
||||
r.has_destination = true;
|
||||
r.destination = str( *d.destination );
|
||||
}
|
||||
if( d.trainset_index.has_value() ) {
|
||||
r.has_trainset = true;
|
||||
r.trainset_index = static_cast<std::uint32_t>( *d.trainset_index );
|
||||
}
|
||||
m_dynamics.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_trainsets() {
|
||||
for( auto const &t : m_scene.trainsets ) {
|
||||
trainset_record r;
|
||||
r.name = str( t.name );
|
||||
r.track = str( t.track );
|
||||
r.offset = t.offset;
|
||||
r.velocity = t.velocity;
|
||||
r.assignment.reserve( t.assignment.size() );
|
||||
for( auto const &kv : t.assignment ) {
|
||||
r.assignment.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
r.vehicle_indices.reserve( t.vehicle_indices.size() );
|
||||
for( auto const idx : t.vehicle_indices ) {
|
||||
r.vehicle_indices.push_back( static_cast<std::uint32_t>( idx ) );
|
||||
}
|
||||
r.couplings.reserve( t.couplings.size() );
|
||||
for( auto const c : t.couplings ) {
|
||||
r.couplings.push_back( c );
|
||||
}
|
||||
r.driver_index =
|
||||
t.driver_index == static_cast<std::size_t>( -1 )
|
||||
? 0xffffffffu
|
||||
: static_cast<std::uint32_t>( t.driver_index );
|
||||
m_trainsets.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_includes() {
|
||||
for( auto const &inc : m_module->includes ) {
|
||||
include_record r;
|
||||
r.source_line = inc.source_line;
|
||||
r.source_path = inc.source_path.empty() ? kNoString : str( inc.source_path );
|
||||
r.binary_path = inc.binary_path.empty() ? kNoString : str( inc.binary_path );
|
||||
r.parameters.reserve( inc.parameters.size() );
|
||||
for( auto const &p : inc.parameters ) {
|
||||
r.parameters.push_back( str( p ) );
|
||||
}
|
||||
r.site_transform = transform( inc.site_transform );
|
||||
m_includes.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] transform_record transform( scene::eu7::Eu7TransformContext const &t ) {
|
||||
transform_record out;
|
||||
out.origin_stack.reserve( t.origin_stack.size() );
|
||||
for( auto const &v : t.origin_stack ) {
|
||||
out.origin_stack.push_back( { v.x, v.y, v.z } );
|
||||
}
|
||||
out.scale_stack.reserve( t.scale_stack.size() );
|
||||
for( auto const &v : t.scale_stack ) {
|
||||
out.scale_stack.push_back( { v.x, v.y, v.z } );
|
||||
}
|
||||
out.rotation = { t.rotation.x, t.rotation.y, t.rotation.z };
|
||||
out.group_depth = static_cast<std::uint32_t>( t.group_depth );
|
||||
return out;
|
||||
}
|
||||
|
||||
void convert_meta() {
|
||||
m_meta.first_init_count = m_scene.first_init_count;
|
||||
m_meta.has_terrain_chunk = m_module->has_terrain_chunk;
|
||||
m_meta.has_pack_chunk = m_module->has_pack_chunk;
|
||||
m_meta.placement_origin_x = m_module->include_placement.origin_x_param;
|
||||
m_meta.placement_origin_y = m_module->include_placement.origin_y_param;
|
||||
m_meta.placement_origin_z = m_module->include_placement.origin_z_param;
|
||||
m_meta.placement_rotation_y = m_module->include_placement.rotation_y_param;
|
||||
m_has_meta = true;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::uint8_t> serialize() const {
|
||||
container_writer writer( file_kind::sim );
|
||||
|
||||
byte_writer strs;
|
||||
m_strings.serialize( strs );
|
||||
writer.add_chunk( chunk::strs, strs );
|
||||
|
||||
// META must be emitted before the chunks it describes are consumed;
|
||||
// a single pass over chunks resolves it (loader reads it whenever seen).
|
||||
if( m_has_meta ) {
|
||||
byte_writer meta;
|
||||
write_meta( meta, m_meta );
|
||||
writer.add_chunk( chunk::meta, meta );
|
||||
}
|
||||
|
||||
auto emit { [&]( std::uint32_t id, auto const &write_fn, auto const &items ) {
|
||||
if( items.empty() ) {
|
||||
return;
|
||||
}
|
||||
byte_writer payload;
|
||||
write_fn( payload, items );
|
||||
writer.add_chunk( id, payload );
|
||||
} };
|
||||
|
||||
emit( chunk::incl, &write_includes, m_includes );
|
||||
emit( chunk::prot, &write_prototypes, m_prototypes );
|
||||
emit( chunk::inst, &write_instances, m_instances );
|
||||
emit( chunk::mesh, &write_terrain_meshes, m_meshes );
|
||||
emit( chunk::shpe, &write_shapes, m_shapes );
|
||||
emit( chunk::line, &write_lines, m_lines );
|
||||
emit( chunk::trak, &write_tracks, m_tracks );
|
||||
emit( chunk::trac, &write_traction, m_traction );
|
||||
emit( chunk::pwrs, &write_power_sources, m_power );
|
||||
emit( chunk::memc, &write_memcells, m_memcells );
|
||||
emit( chunk::laun, &write_launchers, m_launchers );
|
||||
emit( chunk::evnt, &write_events, m_events );
|
||||
emit( chunk::sond, &write_sounds, m_sounds );
|
||||
emit( chunk::dynm, &write_dynamics, m_dynamics );
|
||||
emit( chunk::trst, &write_trainsets, m_trainsets );
|
||||
|
||||
return writer.data();
|
||||
}
|
||||
|
||||
scene::eu7::Eu7Scene const &m_scene;
|
||||
scene::eu7::Eu7Module const *m_module { nullptr };
|
||||
string_table m_strings;
|
||||
|
||||
std::vector<model_prototype> m_prototypes;
|
||||
std::unordered_map<std::string, std::uint32_t> m_prototype_lookup;
|
||||
std::vector<model_instance> m_instances;
|
||||
std::vector<terrain_mesh> m_meshes;
|
||||
std::vector<shape_record> m_shapes;
|
||||
std::vector<lines_record> m_lines;
|
||||
std::vector<track_record> m_tracks;
|
||||
std::vector<traction_record> m_traction;
|
||||
std::vector<power_source_record> m_power;
|
||||
std::vector<memcell_record> m_memcells;
|
||||
std::vector<launcher_record> m_launchers;
|
||||
std::vector<event_record> m_events;
|
||||
std::vector<sound_record> m_sounds;
|
||||
std::vector<dynamic_record> m_dynamics;
|
||||
std::vector<trainset_record> m_trainsets;
|
||||
std::vector<include_record> m_includes;
|
||||
module_meta m_meta;
|
||||
bool m_has_meta { false };
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t>
|
||||
bake_scene( scene::eu7::Eu7Scene const &scene ) {
|
||||
scene_baker baker( scene );
|
||||
return baker.run();
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t>
|
||||
bake_module( scene::eu7::Eu7Module const &module ) {
|
||||
scene_baker baker( module.scene, module );
|
||||
return baker.run();
|
||||
}
|
||||
|
||||
} // namespace eu7v2
|
||||
36
scene/eu7/v2/eu7v2_bake.h
Normal file
36
scene/eu7/v2/eu7v2_bake.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace scene::eu7 {
|
||||
struct Eu7Scene;
|
||||
struct Eu7Module;
|
||||
} // namespace scene::eu7
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
// Bakes a parsed scene (output of the text parser) into a self-contained
|
||||
// eu7v2 "sim" container: STRS + PROT/INST (deduplicated models) + MESH
|
||||
// (terrain) + SHPE/LINE + simulation record chunks. This is the clean-slate
|
||||
// replacement for the legacy EU7B emitter; it reuses the existing parser
|
||||
// front-end and only changes the on-disk representation.
|
||||
[[nodiscard]] std::vector<std::uint8_t>
|
||||
bake_scene( scene::eu7::Eu7Scene const &scene );
|
||||
|
||||
// Lossless module bake: everything bake_scene() emits, plus INCL (includes
|
||||
// for recursion) and META (flags, placement, first_init_count). This is the
|
||||
// format the runtime loads as a module.
|
||||
[[nodiscard]] std::vector<std::uint8_t>
|
||||
bake_module( scene::eu7::Eu7Module const &module );
|
||||
|
||||
} // namespace eu7v2
|
||||
888
scene/eu7/v2/eu7v2_emit_runtime.cpp
Normal file
888
scene/eu7/v2/eu7v2_emit_runtime.cpp
Normal file
@@ -0,0 +1,888 @@
|
||||
/*
|
||||
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 "scene/eu7/v2/eu7v2_emit_runtime.h"
|
||||
|
||||
#include "scene/eu7/v2/eu7v2_format.h"
|
||||
#include "scene/eu7/v2/eu7v2_scene.h"
|
||||
#include "scene/eu7/v2/eu7v2_records.h"
|
||||
|
||||
#include <eu07/scene/bake/pack_model_spool.hpp>
|
||||
#include <eu07/scene/include_resolve.hpp>
|
||||
#include <eu07/scene/runtime/scene.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace rt = eu07::scene::runtime;
|
||||
namespace bake = eu07::scene::bake;
|
||||
namespace codec = eu07::scene::binary::codec;
|
||||
|
||||
[[nodiscard]] bool is_model_inc_placement( bake::ModuleInclude const &inc ) {
|
||||
if( !eu07::scene::detail::isIncFile( inc.sourcePath ) ) {
|
||||
return false;
|
||||
}
|
||||
if( inc.parameters.size() < 5 ) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
(void)std::stod( inc.parameters[ 1 ] );
|
||||
(void)std::stod( inc.parameters[ 2 ] );
|
||||
(void)std::stod( inc.parameters[ 3 ] );
|
||||
(void)std::stod( inc.parameters[ 4 ] );
|
||||
return true;
|
||||
} catch( ... ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool try_parse_inc_placement(
|
||||
std::span<std::string const> const params,
|
||||
double &x,
|
||||
double &y,
|
||||
double &z,
|
||||
float &rot_y ) {
|
||||
if( params.size() < 5 ) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
x = std::stod( params[ 1 ] );
|
||||
y = std::stod( params[ 2 ] );
|
||||
z = std::stod( params[ 3 ] );
|
||||
rot_y = static_cast<float>( std::stod( params[ 4 ] ) );
|
||||
return true;
|
||||
} catch( ... ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] file_kind file_kind_for_text_path( std::filesystem::path const &text_path ) {
|
||||
auto const ext { text_path.extension().string() };
|
||||
if( ext == ".inc" ) {
|
||||
return file_kind::module;
|
||||
}
|
||||
return file_kind::sim;
|
||||
}
|
||||
|
||||
// Mirror of scene_baker but sourced from the parser's Runtime* records.
|
||||
class runtime_baker {
|
||||
public:
|
||||
runtime_baker(
|
||||
bake::RuntimeModule const &module,
|
||||
bool const is_root,
|
||||
std::vector<codec::ModelSectionBatch> const *pack_batches,
|
||||
bake::PackModelSpoolFile const *pack_spool,
|
||||
bake::ShapeSpoolFile const *shape_spool,
|
||||
std::filesystem::path const &text_path )
|
||||
: m_module( module )
|
||||
, m_is_root( is_root )
|
||||
, m_pack_batches( pack_batches )
|
||||
, m_pack_spool( pack_spool )
|
||||
, m_shape_spool( shape_spool )
|
||||
, m_file_kind( file_kind_for_text_path( text_path ) ) {}
|
||||
|
||||
[[nodiscard]] std::vector<std::uint8_t> run() {
|
||||
convert_models();
|
||||
convert_shapes();
|
||||
convert_lines();
|
||||
convert_tracks();
|
||||
convert_traction();
|
||||
convert_power_sources();
|
||||
convert_memcells();
|
||||
convert_launchers();
|
||||
convert_events();
|
||||
convert_sounds();
|
||||
convert_dynamics();
|
||||
convert_trainsets();
|
||||
convert_includes();
|
||||
convert_meta();
|
||||
return serialize();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t model_total() const { return m_instances.size(); }
|
||||
[[nodiscard]] std::size_t placement_total() const { return m_placements.size(); }
|
||||
[[nodiscard]] std::size_t structural_include_total() const { return m_includes.size(); }
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::uint32_t str( std::string const &s ) { return m_strings.intern( s ); }
|
||||
[[nodiscard]] std::uint32_t opt_str( std::string const &s ) {
|
||||
return s.empty() ? kNoString : m_strings.intern( s );
|
||||
}
|
||||
|
||||
[[nodiscard]] node_record node( rt::BasicNode const &n ) {
|
||||
node_record out;
|
||||
out.name = opt_str( n.name );
|
||||
out.type = opt_str( n.nodeType );
|
||||
out.area_center = { n.area.center.x, n.area.center.y, n.area.center.z };
|
||||
out.area_radius = n.area.radius;
|
||||
out.range_sq_min = n.rangeSquaredMin;
|
||||
out.range_sq_max = n.rangeSquaredMax;
|
||||
out.visible = n.visible;
|
||||
return out;
|
||||
}
|
||||
|
||||
[[nodiscard]] lighting_block lighting( rt::LightingData const &l ) {
|
||||
lighting_block out;
|
||||
out.diffuse[ 0 ] = l.diffuse.x; out.diffuse[ 1 ] = l.diffuse.y;
|
||||
out.diffuse[ 2 ] = l.diffuse.z; out.diffuse[ 3 ] = l.diffuse.w;
|
||||
out.ambient[ 0 ] = l.ambient.x; out.ambient[ 1 ] = l.ambient.y;
|
||||
out.ambient[ 2 ] = l.ambient.z; out.ambient[ 3 ] = l.ambient.w;
|
||||
out.specular[ 0 ] = l.specular.x; out.specular[ 1 ] = l.specular.y;
|
||||
out.specular[ 2 ] = l.specular.z; out.specular[ 3 ] = l.specular.w;
|
||||
return out;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t intern_prototype( rt::RuntimeModelInstance const &model ) {
|
||||
std::string key;
|
||||
key.reserve( model.modelFile.size() + model.textureFile.size() + 32 );
|
||||
key.append( model.modelFile );
|
||||
key.push_back( '\x1f' );
|
||||
key.append( model.textureFile );
|
||||
key.push_back( '\x1f' );
|
||||
key.push_back( model.transition ? '1' : '0' );
|
||||
key.push_back( model.isTerrain ? '1' : '0' );
|
||||
for( auto const s : model.lightStates ) {
|
||||
key.append( std::to_string( s ) );
|
||||
key.push_back( ',' );
|
||||
}
|
||||
key.push_back( '\x1f' );
|
||||
for( auto const c : model.lightColors ) {
|
||||
key.append( std::to_string( c ) );
|
||||
key.push_back( ',' );
|
||||
}
|
||||
|
||||
auto const it { m_prototype_lookup.find( key ) };
|
||||
if( it != m_prototype_lookup.end() ) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
model_prototype proto;
|
||||
proto.model_file = str( model.modelFile );
|
||||
proto.texture_file = opt_str( model.textureFile );
|
||||
proto.flags = 0;
|
||||
if( model.transition ) {
|
||||
proto.flags |= proto_flag::transition;
|
||||
}
|
||||
if( model.isTerrain ) {
|
||||
proto.flags |= proto_flag::is_terrain;
|
||||
}
|
||||
proto.range_min = -1.f;
|
||||
proto.range_max = -1.f;
|
||||
proto.light_states = model.lightStates;
|
||||
proto.light_colors = model.lightColors;
|
||||
|
||||
auto const id { static_cast<std::uint32_t>( m_prototypes.size() ) };
|
||||
m_prototypes.push_back( std::move( proto ) );
|
||||
m_prototype_lookup.emplace( std::move( key ), id );
|
||||
return id;
|
||||
}
|
||||
|
||||
void emit_model( rt::RuntimeModelInstance const &model ) {
|
||||
auto const proto { intern_prototype( model ) };
|
||||
model_instance inst;
|
||||
inst.proto = proto;
|
||||
inst.x = model.location.x;
|
||||
inst.y = model.location.y;
|
||||
inst.z = model.location.z;
|
||||
inst.ax = static_cast<float>( model.angles.x );
|
||||
inst.ay = static_cast<float>( model.angles.y );
|
||||
inst.az = static_cast<float>( model.angles.z );
|
||||
inst.sx = static_cast<float>( model.scale.x );
|
||||
inst.sy = static_cast<float>( model.scale.y );
|
||||
inst.sz = static_cast<float>( model.scale.z );
|
||||
inst.cell_id = 0xffu;
|
||||
inst.texture_override = kNoString;
|
||||
inst.has_node = true;
|
||||
inst.node = node( model.node );
|
||||
m_instances.push_back( std::move( inst ) );
|
||||
}
|
||||
|
||||
void convert_models() {
|
||||
if( m_is_root && m_pack_spool != nullptr ) {
|
||||
m_pack_spool->for_each_model( [&]( rt::RuntimeModelInstance const &model ) {
|
||||
emit_model( model );
|
||||
} );
|
||||
}
|
||||
if( m_is_root && m_pack_batches != nullptr ) {
|
||||
// Root: models live in the flattened PACK batches, not scene.models.
|
||||
for( auto const &batch : *m_pack_batches ) {
|
||||
for( auto const &model : batch.models ) {
|
||||
emit_model( model );
|
||||
}
|
||||
}
|
||||
}
|
||||
for( auto const &model : m_module.scene.models ) {
|
||||
emit_model( model );
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] shape_record make_shape_record( rt::RuntimeShapeNode const &shape ) {
|
||||
shape_record r;
|
||||
r.node = node( shape.node );
|
||||
r.translucent = shape.translucent;
|
||||
r.material = opt_str( shape.materialPath );
|
||||
r.lighting = lighting( shape.lighting );
|
||||
r.ox = shape.origin.x;
|
||||
r.oy = shape.origin.y;
|
||||
r.oz = shape.origin.z;
|
||||
r.vertices.reserve( shape.vertices.size() );
|
||||
for( auto const &v : shape.vertices ) {
|
||||
mesh_vertex mv;
|
||||
mv.px = static_cast<float>( v.position.x - shape.origin.x );
|
||||
mv.py = static_cast<float>( v.position.y - shape.origin.y );
|
||||
mv.pz = static_cast<float>( v.position.z - shape.origin.z );
|
||||
mv.nx = static_cast<float>( v.normal.x );
|
||||
mv.ny = static_cast<float>( v.normal.y );
|
||||
mv.nz = static_cast<float>( v.normal.z );
|
||||
mv.u = static_cast<float>( v.u );
|
||||
mv.v = static_cast<float>( v.v );
|
||||
r.vertices.push_back( mv );
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void convert_shapes() {
|
||||
if( m_shape_spool != nullptr ) {
|
||||
m_shape_spool->for_each_shape( [&]( rt::RuntimeShapeNode const &shape ) {
|
||||
(void)opt_str( shape.materialPath );
|
||||
(void)node( shape.node );
|
||||
} );
|
||||
return;
|
||||
}
|
||||
for( auto const &shape : m_module.scene.shapes ) {
|
||||
m_shapes.push_back( make_shape_record( shape ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_lines() {
|
||||
for( auto const &line : m_module.scene.lines ) {
|
||||
lines_record r;
|
||||
r.node = node( line.node );
|
||||
r.lighting = lighting( line.lighting );
|
||||
r.line_width = line.lineWidth;
|
||||
r.ox = line.origin.x;
|
||||
r.oy = line.origin.y;
|
||||
r.oz = line.origin.z;
|
||||
r.vertices.reserve( line.vertices.size() );
|
||||
for( auto const &v : line.vertices ) {
|
||||
r.vertices.push_back( { v.position.x, v.position.y, v.position.z } );
|
||||
}
|
||||
m_lines.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_tracks() {
|
||||
for( auto const &t : m_module.scene.tracks ) {
|
||||
track_record r;
|
||||
r.node = node( t.node );
|
||||
r.track_type = static_cast<std::uint8_t>( t.trackType );
|
||||
r.category = static_cast<std::uint8_t>( t.category );
|
||||
r.length = t.length;
|
||||
r.track_width = t.trackWidth;
|
||||
r.friction = t.friction;
|
||||
r.sound_distance = t.soundDistance;
|
||||
r.quality_flag = t.qualityFlag;
|
||||
r.damage_flag = t.damageFlag;
|
||||
r.environment = static_cast<std::int8_t>( t.environment );
|
||||
if( t.visibility.has_value() ) {
|
||||
r.has_visibility = true;
|
||||
r.visibility.material1 = str( t.visibility->material1 );
|
||||
r.visibility.tex_length = t.visibility->texLength;
|
||||
r.visibility.material2 = str( t.visibility->material2 );
|
||||
r.visibility.tex_height1 = t.visibility->texHeight1;
|
||||
r.visibility.tex_width = t.visibility->texWidth;
|
||||
r.visibility.tex_slope = t.visibility->texSlope;
|
||||
}
|
||||
r.paths.reserve( t.paths.size() );
|
||||
for( auto const &p : t.paths ) {
|
||||
track_path tp;
|
||||
tp.p_start = { p.pStart.x, p.pStart.y, p.pStart.z };
|
||||
tp.roll_start = p.rollStart;
|
||||
tp.cp_out = { p.cpOut.x, p.cpOut.y, p.cpOut.z };
|
||||
tp.cp_in = { p.cpIn.x, p.cpIn.y, p.cpIn.z };
|
||||
tp.p_end = { p.pEnd.x, p.pEnd.y, p.pEnd.z };
|
||||
tp.roll_end = p.rollEnd;
|
||||
tp.radius = p.radius;
|
||||
r.paths.push_back( tp );
|
||||
}
|
||||
r.tail_keywords.reserve( t.tailKeywords.size() );
|
||||
for( auto const &kv : t.tailKeywords ) {
|
||||
r.tail_keywords.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
m_tracks.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_traction() {
|
||||
for( auto const &t : m_module.scene.traction ) {
|
||||
traction_record r;
|
||||
r.node = node( t.node );
|
||||
r.power_supply_name = opt_str( t.powerSupplyName );
|
||||
r.nominal_voltage = t.nominalVoltage;
|
||||
r.max_current = t.maxCurrent;
|
||||
r.resistivity = t.resistivityOhmPerM;
|
||||
r.material = static_cast<std::uint8_t>( t.material );
|
||||
r.wire_thickness = t.wireThickness;
|
||||
r.damage_flag = t.damageFlag;
|
||||
r.wire_p1 = { t.wireP1.x, t.wireP1.y, t.wireP1.z };
|
||||
r.wire_p2 = { t.wireP2.x, t.wireP2.y, t.wireP2.z };
|
||||
r.wire_p3 = { t.wireP3.x, t.wireP3.y, t.wireP3.z };
|
||||
r.wire_p4 = { t.wireP4.x, t.wireP4.y, t.wireP4.z };
|
||||
r.min_height = t.minHeight;
|
||||
r.segment_length = t.segmentLength;
|
||||
r.wire_count = t.wireCount;
|
||||
r.wire_offset = t.wireOffset;
|
||||
if( t.parallelName.has_value() ) {
|
||||
r.has_parallel = true;
|
||||
r.parallel_name = str( *t.parallelName );
|
||||
}
|
||||
m_traction.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_power_sources() {
|
||||
for( auto const &p : m_module.scene.powerSources ) {
|
||||
power_source_record r;
|
||||
r.node = node( p.node );
|
||||
r.position = { p.position.x, p.position.y, p.position.z };
|
||||
r.nominal_voltage = p.nominalVoltage;
|
||||
r.voltage_frequency = p.voltageFrequency;
|
||||
r.internal_resistance = p.internalResistance;
|
||||
r.max_output_current = p.maxOutputCurrent;
|
||||
r.fast_fuse_timeout = p.fastFuseTimeout;
|
||||
r.fast_fuse_repetition = p.fastFuseRepetition;
|
||||
r.slow_fuse_timeout = p.slowFuseTimeout;
|
||||
r.modifier = static_cast<std::uint8_t>( p.modifier );
|
||||
m_power.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_memcells() {
|
||||
for( auto const &m : m_module.scene.memcells ) {
|
||||
memcell_record r;
|
||||
r.node = node( m.node );
|
||||
r.text = opt_str( m.text );
|
||||
r.value1 = m.value1;
|
||||
r.value2 = m.value2;
|
||||
if( m.trackName.has_value() ) {
|
||||
r.has_track = true;
|
||||
r.track_name = str( *m.trackName );
|
||||
}
|
||||
m_memcells.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_launchers() {
|
||||
for( auto const &l : m_module.scene.eventLaunchers ) {
|
||||
launcher_record r;
|
||||
r.node = node( l.node );
|
||||
r.location = { l.location.x, l.location.y, l.location.z };
|
||||
r.radius_squared = l.radiusSquared;
|
||||
r.activation_key = l.activationKey;
|
||||
r.delta_time = l.deltaTime;
|
||||
r.event1_name = opt_str( l.event1Name );
|
||||
r.event2_name = opt_str( l.event2Name );
|
||||
if( l.condition.has_value() ) {
|
||||
r.has_condition = true;
|
||||
r.condition.memcell_name = str( l.condition->memcellName );
|
||||
r.condition.compare_text = str( l.condition->compareText );
|
||||
r.condition.compare_value1 = l.condition->compareValue1;
|
||||
r.condition.compare_value2 = l.condition->compareValue2;
|
||||
r.condition.check_mask = l.condition->checkMask;
|
||||
}
|
||||
r.train_triggered = l.trainTriggered;
|
||||
r.launch_hour = l.launchHour;
|
||||
r.launch_minute = l.launchMinute;
|
||||
m_launchers.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_events() {
|
||||
for( auto const &e : m_module.scene.events ) {
|
||||
event_record r;
|
||||
r.name = opt_str( e.name );
|
||||
r.type = static_cast<std::uint8_t>( e.type );
|
||||
r.delay = e.delay;
|
||||
r.delay_random = e.delayRandom;
|
||||
r.delay_departure = e.delayDeparture;
|
||||
r.ignored = e.ignored;
|
||||
r.passive = e.passive;
|
||||
r.targets.reserve( e.targets.size() );
|
||||
for( auto const &t : e.targets ) {
|
||||
r.targets.push_back( str( t ) );
|
||||
}
|
||||
r.payload.reserve( e.payload.size() );
|
||||
for( auto const &kv : e.payload ) {
|
||||
r.payload.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
m_events.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_sounds() {
|
||||
for( auto const &s : m_module.scene.sounds ) {
|
||||
sound_record r;
|
||||
r.node = node( s.node );
|
||||
r.location = { s.location.x, s.location.y, s.location.z };
|
||||
r.wav_file = opt_str( s.wavFile );
|
||||
m_sounds.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_dynamics() {
|
||||
for( auto const &d : m_module.scene.dynamics ) {
|
||||
dynamic_record r;
|
||||
r.node = node( d.node );
|
||||
r.data_folder = opt_str( d.dataFolder );
|
||||
r.skin_file = opt_str( d.skinFile );
|
||||
r.mmd_file = opt_str( d.mmdFile );
|
||||
r.track_name = opt_str( d.trackName );
|
||||
r.offset = d.offset;
|
||||
r.driver_type = opt_str( d.driverType );
|
||||
r.coupling = d.coupling;
|
||||
r.coupling_raw = opt_str( d.couplingRaw );
|
||||
r.coupling_params = opt_str( d.couplingParams );
|
||||
r.velocity = d.velocity;
|
||||
r.load_count = d.loadCount;
|
||||
r.load_type = opt_str( d.loadType );
|
||||
if( d.destination.has_value() ) {
|
||||
r.has_destination = true;
|
||||
r.destination = str( *d.destination );
|
||||
}
|
||||
if( d.trainsetIndex.has_value() ) {
|
||||
r.has_trainset = true;
|
||||
r.trainset_index = static_cast<std::uint32_t>( *d.trainsetIndex );
|
||||
}
|
||||
m_dynamics.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_trainsets() {
|
||||
for( auto const &t : m_module.scene.trainsets ) {
|
||||
trainset_record r;
|
||||
r.name = opt_str( t.name );
|
||||
r.track = opt_str( t.track );
|
||||
r.offset = t.offset;
|
||||
r.velocity = t.velocity;
|
||||
r.assignment.reserve( t.assignment.size() );
|
||||
for( auto const &kv : t.assignment ) {
|
||||
r.assignment.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
r.vehicle_indices.reserve( t.vehicleIndices.size() );
|
||||
for( auto const idx : t.vehicleIndices ) {
|
||||
r.vehicle_indices.push_back( static_cast<std::uint32_t>( idx ) );
|
||||
}
|
||||
r.couplings.reserve( t.couplings.size() );
|
||||
for( auto const c : t.couplings ) {
|
||||
r.couplings.push_back( c );
|
||||
}
|
||||
r.driver_index =
|
||||
t.driverIndex == static_cast<std::size_t>( -1 )
|
||||
? 0xffffffffu
|
||||
: static_cast<std::uint32_t>( t.driverIndex );
|
||||
m_trainsets.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] transform_record transform( rt::TransformContext const &t ) {
|
||||
transform_record out;
|
||||
out.origin_stack.reserve( t.originStack.size() );
|
||||
for( auto const &v : t.originStack ) {
|
||||
out.origin_stack.push_back( { v.x, v.y, v.z } );
|
||||
}
|
||||
out.scale_stack.reserve( t.scaleStack.size() );
|
||||
for( auto const &v : t.scaleStack ) {
|
||||
out.scale_stack.push_back( { v.x, v.y, v.z } );
|
||||
}
|
||||
out.rotation = { t.rotation.x, t.rotation.y, t.rotation.z };
|
||||
out.group_depth = static_cast<std::uint32_t>( t.groupStackDepth );
|
||||
return out;
|
||||
}
|
||||
|
||||
void convert_includes() {
|
||||
for( auto const &inc : m_module.includes ) {
|
||||
if( is_model_inc_placement( inc ) ) {
|
||||
double x { 0.0 }, y { 0.0 }, z { 0.0 };
|
||||
float rot_y { 0.f };
|
||||
if( !try_parse_inc_placement( inc.parameters, x, y, z, rot_y ) ) {
|
||||
continue;
|
||||
}
|
||||
module_placement_record p;
|
||||
p.module_path = str(
|
||||
binary_path_from_text( std::filesystem::path { inc.sourcePath } )
|
||||
.generic_string() );
|
||||
p.texture_override =
|
||||
( inc.parameters.empty() || inc.parameters[ 0 ] == "none" )
|
||||
? kNoString
|
||||
: str( inc.parameters[ 0 ] );
|
||||
p.x = x;
|
||||
p.y = y;
|
||||
p.z = z;
|
||||
p.rotation_y = rot_y;
|
||||
m_placements.push_back( std::move( p ) );
|
||||
continue;
|
||||
}
|
||||
include_record r;
|
||||
r.source_line = inc.sourceLine;
|
||||
r.source_path = opt_str( inc.sourcePath );
|
||||
r.binary_path = inc.sourcePath.empty()
|
||||
? kNoString
|
||||
: str( binary_path_from_text(
|
||||
std::filesystem::path { inc.sourcePath } )
|
||||
.generic_string() );
|
||||
r.parameters.reserve( inc.parameters.size() );
|
||||
for( auto const ¶m : inc.parameters ) {
|
||||
r.parameters.push_back( str( param ) );
|
||||
}
|
||||
r.site_transform = transform( inc.siteTransform );
|
||||
m_includes.push_back( std::move( r ) );
|
||||
}
|
||||
}
|
||||
|
||||
void convert_meta() {
|
||||
m_meta.first_init_count = m_module.scene.firstInitCount;
|
||||
m_meta.has_terrain_chunk = false;
|
||||
// Root PACK batches are flattened into plain INST records, so the file
|
||||
// carries no PACK streaming chunk; never advertise one.
|
||||
m_meta.has_pack_chunk = false;
|
||||
m_meta.placement_origin_x = m_module.includePlacement.origin_x_param;
|
||||
m_meta.placement_origin_y = m_module.includePlacement.origin_y_param;
|
||||
m_meta.placement_origin_z = m_module.includePlacement.origin_z_param;
|
||||
m_meta.placement_rotation_y = m_module.includePlacement.rotation_y_param;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::uint8_t> serialize() {
|
||||
container_writer writer( m_file_kind );
|
||||
|
||||
byte_writer strs;
|
||||
m_strings.serialize( strs );
|
||||
writer.add_chunk( chunk::strs, strs );
|
||||
|
||||
byte_writer meta;
|
||||
write_meta( meta, m_meta );
|
||||
writer.add_chunk( chunk::meta, meta );
|
||||
|
||||
auto emit { [&]( std::uint32_t id, auto const &write_fn, auto const &items ) {
|
||||
if( items.empty() ) {
|
||||
return;
|
||||
}
|
||||
byte_writer payload;
|
||||
write_fn( payload, items );
|
||||
writer.add_chunk( id, payload );
|
||||
} };
|
||||
|
||||
emit( chunk::incl, &write_includes, m_includes );
|
||||
emit( chunk::plce, &write_module_placements, m_placements );
|
||||
emit( chunk::prot, &write_prototypes, m_prototypes );
|
||||
emit( chunk::inst, &write_instances, m_instances );
|
||||
if( m_shape_spool != nullptr && m_shape_spool->shape_count() != 0 ) {
|
||||
byte_writer payload;
|
||||
payload.put_u32( static_cast<std::uint32_t>( m_shape_spool->shape_count() ) );
|
||||
m_shape_spool->for_each_shape( [&]( rt::RuntimeShapeNode const &shape ) {
|
||||
write_shape_record( payload, make_shape_record( shape ) );
|
||||
} );
|
||||
writer.add_chunk( chunk::shpe, payload );
|
||||
} else {
|
||||
emit( chunk::shpe, &write_shapes, m_shapes );
|
||||
}
|
||||
emit( chunk::line, &write_lines, m_lines );
|
||||
emit( chunk::trak, &write_tracks, m_tracks );
|
||||
emit( chunk::trac, &write_traction, m_traction );
|
||||
emit( chunk::pwrs, &write_power_sources, m_power );
|
||||
emit( chunk::memc, &write_memcells, m_memcells );
|
||||
emit( chunk::laun, &write_launchers, m_launchers );
|
||||
emit( chunk::evnt, &write_events, m_events );
|
||||
emit( chunk::sond, &write_sounds, m_sounds );
|
||||
emit( chunk::dynm, &write_dynamics, m_dynamics );
|
||||
emit( chunk::trst, &write_trainsets, m_trainsets );
|
||||
|
||||
return writer.data();
|
||||
}
|
||||
|
||||
bake::RuntimeModule const &m_module;
|
||||
bool m_is_root;
|
||||
std::vector<codec::ModelSectionBatch> const *m_pack_batches;
|
||||
bake::PackModelSpoolFile const *m_pack_spool;
|
||||
bake::ShapeSpoolFile const *m_shape_spool;
|
||||
file_kind m_file_kind { file_kind::sim };
|
||||
std::vector<module_placement_record> m_placements;
|
||||
string_table m_strings;
|
||||
|
||||
std::vector<model_prototype> m_prototypes;
|
||||
std::unordered_map<std::string, std::uint32_t> m_prototype_lookup;
|
||||
std::vector<model_instance> m_instances;
|
||||
std::vector<shape_record> m_shapes;
|
||||
std::vector<lines_record> m_lines;
|
||||
std::vector<track_record> m_tracks;
|
||||
std::vector<traction_record> m_traction;
|
||||
std::vector<power_source_record> m_power;
|
||||
std::vector<memcell_record> m_memcells;
|
||||
std::vector<launcher_record> m_launchers;
|
||||
std::vector<event_record> m_events;
|
||||
std::vector<sound_record> m_sounds;
|
||||
std::vector<dynamic_record> m_dynamics;
|
||||
std::vector<trainset_record> m_trainsets;
|
||||
std::vector<include_record> m_includes;
|
||||
module_meta m_meta;
|
||||
};
|
||||
|
||||
// Decoded record counts of an emitted eu7v2 image (chunk-by-chunk).
|
||||
struct decoded_counts {
|
||||
std::size_t includes { 0 };
|
||||
std::size_t placements { 0 };
|
||||
std::size_t instances { 0 };
|
||||
std::size_t shapes { 0 };
|
||||
std::size_t lines { 0 };
|
||||
std::size_t tracks { 0 };
|
||||
std::size_t traction { 0 };
|
||||
std::size_t power { 0 };
|
||||
std::size_t memcells { 0 };
|
||||
std::size_t launchers { 0 };
|
||||
std::size_t events { 0 };
|
||||
std::size_t sounds { 0 };
|
||||
std::size_t dynamics { 0 };
|
||||
std::size_t trainsets { 0 };
|
||||
bool ok { true };
|
||||
};
|
||||
|
||||
[[nodiscard]] decoded_counts decode_counts( std::vector<std::uint8_t> const &bytes ) {
|
||||
decoded_counts c;
|
||||
try {
|
||||
container_reader reader( bytes.data(), bytes.size() );
|
||||
chunk_view chunk;
|
||||
while( reader.next( chunk ) ) {
|
||||
auto r { chunk.reader() };
|
||||
switch( chunk.id ) {
|
||||
case chunk::incl: c.includes = read_includes( r ).size(); break;
|
||||
case chunk::plce: c.placements = read_module_placements( r ).size(); break;
|
||||
case chunk::inst: c.instances = read_instances( r ).size(); break;
|
||||
case chunk::shpe: c.shapes = read_shapes( r ).size(); break;
|
||||
case chunk::line: c.lines = read_lines( r ).size(); break;
|
||||
case chunk::trak: c.tracks = read_tracks( r ).size(); break;
|
||||
case chunk::trac: c.traction = read_traction( r ).size(); break;
|
||||
case chunk::pwrs: c.power = read_power_sources( r ).size(); break;
|
||||
case chunk::memc: c.memcells = read_memcells( r ).size(); break;
|
||||
case chunk::laun: c.launchers = read_launchers( r ).size(); break;
|
||||
case chunk::evnt: c.events = read_events( r ).size(); break;
|
||||
case chunk::sond: c.sounds = read_sounds( r ).size(); break;
|
||||
case chunk::dynm: c.dynamics = read_dynamics( r ).size(); break;
|
||||
case chunk::trst: c.trainsets = read_trainsets( r ).size(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( parse_error const & ) {
|
||||
c.ok = false;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::filesystem::path eu7v2_path_for( std::filesystem::path const &text_path ) {
|
||||
return binary_path_from_text( text_path );
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t>
|
||||
emit_runtime_module_bytes(
|
||||
bake::RuntimeModule const &module,
|
||||
bool const is_root,
|
||||
std::vector<codec::ModelSectionBatch> const *pack_batches,
|
||||
bake::PackModelSpoolFile const *pack_spool,
|
||||
bake::ShapeSpoolFile const *shape_spool,
|
||||
std::filesystem::path const &text_path ) {
|
||||
runtime_baker baker( module, is_root, pack_batches, pack_spool, shape_spool, text_path );
|
||||
return baker.run();
|
||||
}
|
||||
|
||||
emit_outcome
|
||||
emit_runtime_module(
|
||||
bake::RuntimeModule const &module,
|
||||
std::filesystem::path const &text_path,
|
||||
bool const is_root,
|
||||
std::vector<codec::ModelSectionBatch> const *pack_batches,
|
||||
bool const verify,
|
||||
bake::PackModelSpoolFile const *pack_spool,
|
||||
bake::ShapeSpoolFile const *shape_spool ) {
|
||||
emit_outcome outcome;
|
||||
|
||||
auto const ms_since { []( std::chrono::steady_clock::time_point const t0 ) {
|
||||
return std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - t0 )
|
||||
.count();
|
||||
} };
|
||||
|
||||
auto const build_begin { std::chrono::steady_clock::now() };
|
||||
runtime_baker baker( module, is_root, pack_batches, pack_spool, shape_spool, text_path );
|
||||
std::vector<std::uint8_t> const bytes { baker.run() };
|
||||
outcome.model_total = baker.model_total();
|
||||
outcome.byte_size = bytes.size();
|
||||
outcome.build_ms = ms_since( build_begin );
|
||||
|
||||
auto const out_path { eu7v2_path_for( text_path ) };
|
||||
outcome.written_path = out_path.generic_string();
|
||||
|
||||
{
|
||||
auto const write_begin { std::chrono::steady_clock::now() };
|
||||
std::ofstream output { out_path, std::ios::binary | std::ios::trunc };
|
||||
if( !output ) {
|
||||
outcome.ok = false;
|
||||
outcome.message = "nie mozna zapisac " + outcome.written_path;
|
||||
return outcome;
|
||||
}
|
||||
output.write(
|
||||
reinterpret_cast<char const *>( bytes.data() ),
|
||||
static_cast<std::streamsize>( bytes.size() ) );
|
||||
output.flush();
|
||||
outcome.write_ms = ms_since( write_begin );
|
||||
}
|
||||
|
||||
if( !verify ) {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
auto const verify_begin { std::chrono::steady_clock::now() };
|
||||
outcome.verified = true;
|
||||
decoded_counts const dec { decode_counts( bytes ) };
|
||||
|
||||
std::size_t src_models { module.scene.models.size() };
|
||||
if( is_root && pack_spool != nullptr ) {
|
||||
src_models += pack_spool->model_count();
|
||||
}
|
||||
if( is_root && pack_batches != nullptr ) {
|
||||
for( auto const &batch : *pack_batches ) {
|
||||
src_models += batch.models.size();
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t src_placements { 0 };
|
||||
std::size_t src_structural { 0 };
|
||||
for( auto const &inc : module.includes ) {
|
||||
if( is_model_inc_placement( inc ) ) {
|
||||
++src_placements;
|
||||
} else {
|
||||
++src_structural;
|
||||
}
|
||||
}
|
||||
|
||||
std::ostringstream report;
|
||||
bool pass { dec.ok };
|
||||
auto cmp { [&]( char const *label, std::size_t const src, std::size_t const got ) {
|
||||
bool const ok { src == got };
|
||||
pass = pass && ok;
|
||||
report << " " << ( ok ? "ok " : "FAIL" ) << ' ' << label << " src=" << src
|
||||
<< " eu7v2=" << got << '\n';
|
||||
} };
|
||||
|
||||
cmp( "includes", src_structural, dec.includes );
|
||||
cmp( "placements", src_placements, dec.placements );
|
||||
cmp( "models", src_models, dec.instances );
|
||||
cmp( "shapes", module.scene.shapes.size() + ( shape_spool != nullptr ? shape_spool->shape_count() : 0 ),
|
||||
dec.shapes );
|
||||
cmp( "lines", module.scene.lines.size(), dec.lines );
|
||||
cmp( "tracks", module.scene.tracks.size(), dec.tracks );
|
||||
cmp( "traction", module.scene.traction.size(), dec.traction );
|
||||
cmp( "power", module.scene.powerSources.size(), dec.power );
|
||||
cmp( "memcells", module.scene.memcells.size(), dec.memcells );
|
||||
cmp( "launchers", module.scene.eventLaunchers.size(), dec.launchers );
|
||||
cmp( "events", module.scene.events.size(), dec.events );
|
||||
cmp( "sounds", module.scene.sounds.size(), dec.sounds );
|
||||
cmp( "dynamics", module.scene.dynamics.size(), dec.dynamics );
|
||||
cmp( "trainsets", module.scene.trainsets.size(), dec.trainsets );
|
||||
|
||||
outcome.verify_ok = pass;
|
||||
outcome.message = report.str();
|
||||
outcome.verify_ms = ms_since( verify_begin );
|
||||
return outcome;
|
||||
}
|
||||
|
||||
bool
|
||||
verify_written_module(
|
||||
std::filesystem::path const &path,
|
||||
module_verify_spec const &spec,
|
||||
bool const is_root,
|
||||
std::size_t const pack_models,
|
||||
std::string *message_out ) {
|
||||
std::ifstream input { path, std::ios::binary };
|
||||
if( !input ) {
|
||||
if( message_out != nullptr ) {
|
||||
*message_out = "nie mozna odczytac " + path.generic_string();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
input.seekg( 0, std::ios::end );
|
||||
const std::streamoff file_size { input.tellg() };
|
||||
input.seekg( 0, std::ios::beg );
|
||||
if( file_size <= 0 ) {
|
||||
if( message_out != nullptr ) {
|
||||
*message_out = "pusty plik " + path.generic_string();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes( static_cast<std::size_t>( file_size ) );
|
||||
input.read(
|
||||
reinterpret_cast<char *>( bytes.data() ),
|
||||
static_cast<std::streamsize>( bytes.size() ) );
|
||||
|
||||
decoded_counts const dec { decode_counts( bytes ) };
|
||||
if( !dec.ok ) {
|
||||
if( message_out != nullptr ) {
|
||||
*message_out = "decode blad " + path.generic_string();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t src_models { spec.models };
|
||||
if( is_root ) {
|
||||
src_models += pack_models;
|
||||
}
|
||||
|
||||
std::ostringstream report;
|
||||
bool pass { dec.ok };
|
||||
auto cmp { [&]( char const *label, std::size_t const src, std::size_t const got ) {
|
||||
bool const ok { src == got };
|
||||
pass = pass && ok;
|
||||
report << " " << ( ok ? "ok " : "FAIL" ) << ' ' << label << " src=" << src
|
||||
<< " eu7v2=" << got << '\n';
|
||||
} };
|
||||
|
||||
cmp( "includes", spec.includes, dec.includes );
|
||||
cmp( "placements", spec.placements, dec.placements );
|
||||
cmp( "models", src_models, dec.instances );
|
||||
cmp( "shapes", spec.shapes, dec.shapes );
|
||||
cmp( "lines", spec.lines, dec.lines );
|
||||
cmp( "tracks", spec.tracks, dec.tracks );
|
||||
cmp( "traction", spec.traction, dec.traction );
|
||||
cmp( "power", spec.power, dec.power );
|
||||
cmp( "memcells", spec.memcells, dec.memcells );
|
||||
cmp( "launchers", spec.launchers, dec.launchers );
|
||||
cmp( "events", spec.events, dec.events );
|
||||
cmp( "sounds", spec.sounds, dec.sounds );
|
||||
cmp( "dynamics", spec.dynamics, dec.dynamics );
|
||||
cmp( "trainsets", spec.trainsets, dec.trainsets );
|
||||
|
||||
if( message_out != nullptr ) {
|
||||
*message_out = report.str();
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
} // namespace eu7v2
|
||||
99
scene/eu7/v2/eu7v2_emit_runtime.h
Normal file
99
scene/eu7/v2/eu7v2_emit_runtime.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RuntimeScene -> eu7v2 emitter. Lives in the eu07_bake target because it
|
||||
// touches the parser's RuntimeModule types directly. It maps Runtime* records
|
||||
// into the same dependency-free eu7v2 structs that scene_baker uses (one
|
||||
// writer/reader per record), so the produced .eu7v2 matches what the engine
|
||||
// loads via eu7v2::load_module. For the scenario root the flattened PACK model
|
||||
// batches are deduplicated into prototypes + instances.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include <eu07/scene/bake/module.hpp>
|
||||
#include <eu07/scene/binary/runtime_codec.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace eu07::scene::bake {
|
||||
class PackModelSpoolFile;
|
||||
class ShapeSpoolFile;
|
||||
}
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
struct emit_outcome {
|
||||
bool ok { true };
|
||||
bool verified { false };
|
||||
bool verify_ok { true };
|
||||
std::string written_path;
|
||||
std::string message;
|
||||
std::size_t model_total { 0 };
|
||||
std::size_t byte_size { 0 };
|
||||
double build_ms { 0.0 };
|
||||
double write_ms { 0.0 };
|
||||
double verify_ms { 0.0 };
|
||||
};
|
||||
|
||||
struct module_verify_spec {
|
||||
std::size_t includes { 0 };
|
||||
std::size_t placements { 0 };
|
||||
std::size_t models { 0 };
|
||||
std::size_t shapes { 0 };
|
||||
std::size_t lines { 0 };
|
||||
std::size_t tracks { 0 };
|
||||
std::size_t traction { 0 };
|
||||
std::size_t power { 0 };
|
||||
std::size_t memcells { 0 };
|
||||
std::size_t launchers { 0 };
|
||||
std::size_t events { 0 };
|
||||
std::size_t sounds { 0 };
|
||||
std::size_t dynamics { 0 };
|
||||
std::size_t trainsets { 0 };
|
||||
};
|
||||
|
||||
// Builds the eu7v2 byte image for one baked module.
|
||||
[[nodiscard]] std::vector<std::uint8_t>
|
||||
emit_runtime_module_bytes(
|
||||
eu07::scene::bake::RuntimeModule const &module,
|
||||
bool is_root,
|
||||
std::vector<eu07::scene::binary::codec::ModelSectionBatch> const *pack_batches,
|
||||
eu07::scene::bake::PackModelSpoolFile const *pack_spool = nullptr,
|
||||
eu07::scene::bake::ShapeSpoolFile const *shape_spool = nullptr,
|
||||
std::filesystem::path const &text_path = {} );
|
||||
|
||||
// Emits the module, writes "<stem>.eu7v2" next to the text source, and (when
|
||||
// verify is true) reloads the bytes and compares record counts against the
|
||||
// source RuntimeModule, filling outcome.message with a PASS/FAIL report.
|
||||
[[nodiscard]] emit_outcome
|
||||
emit_runtime_module(
|
||||
eu07::scene::bake::RuntimeModule const &module,
|
||||
std::filesystem::path const &text_path,
|
||||
bool is_root,
|
||||
std::vector<eu07::scene::binary::codec::ModelSectionBatch> const *pack_batches,
|
||||
bool verify,
|
||||
eu07::scene::bake::PackModelSpoolFile const *pack_spool = nullptr,
|
||||
eu07::scene::bake::ShapeSpoolFile const *shape_spool = nullptr );
|
||||
|
||||
// Porownanie zapisanej eu7v2 z oczekiwanymi licznikami (do odroczonego verify).
|
||||
[[nodiscard]] bool
|
||||
verify_written_module(
|
||||
std::filesystem::path const &path,
|
||||
module_verify_spec const &spec,
|
||||
bool const is_root,
|
||||
std::size_t const pack_models,
|
||||
std::string *message_out = nullptr );
|
||||
|
||||
} // namespace eu7v2
|
||||
383
scene/eu7/v2/eu7v2_format.h
Normal file
383
scene/eu7/v2/eu7v2_format.h
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// eu7v2 - clean-slate compiled scenery format core (no backward compatibility
|
||||
// with the legacy EU7B v4..v13 chunks).
|
||||
//
|
||||
// Design goals:
|
||||
// * self-describing, skippable chunks: [u32 id][u64 size][payload]
|
||||
// * explicit little-endian on disk so files are deterministic & portable
|
||||
// * a central string table so payloads reference strings by u32 id
|
||||
// * a separation of file kinds: sim core / reusable module / streamable tile
|
||||
// * header-only and dependency-free so it can be unit tested in isolation
|
||||
//
|
||||
// This is the foundation layer only (iteration 1). Higher layers (baker,
|
||||
// runtime loader, streaming) build on top of these primitives.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
// FourCC packed little-endian: 'A' is the least significant byte so the bytes
|
||||
// appear in source order when the file is viewed in a hex editor.
|
||||
[[nodiscard]] constexpr std::uint32_t
|
||||
fourcc( char const a, char const b, char const c, char const d ) noexcept {
|
||||
return ( static_cast<std::uint32_t>( static_cast<unsigned char>( a ) ) )
|
||||
| ( static_cast<std::uint32_t>( static_cast<unsigned char>( b ) ) << 8 )
|
||||
| ( static_cast<std::uint32_t>( static_cast<unsigned char>( c ) ) << 16 )
|
||||
| ( static_cast<std::uint32_t>( static_cast<unsigned char>( d ) ) << 24 );
|
||||
}
|
||||
|
||||
// File magic + single current version. No legacy versions are recognised.
|
||||
constexpr std::uint32_t kMagic { fourcc( 'E', 'U', '7', 'C' ) };
|
||||
constexpr std::uint16_t kVersion { 1 };
|
||||
|
||||
enum class file_kind : std::uint16_t {
|
||||
sim = 1, // global simulation core, loaded once
|
||||
module = 2, // reusable module / prototype library (baked .inc)
|
||||
tile = 3, // streamable 1km visual tile (terrain + instances)
|
||||
manifest = 4, // index of tiles & modules for a map
|
||||
};
|
||||
|
||||
// Chunk identifiers (FourCC). Kept small & explicit; grows as layers land.
|
||||
namespace chunk {
|
||||
constexpr std::uint32_t strs { fourcc( 'S', 'T', 'R', 'S' ) }; // string table
|
||||
constexpr std::uint32_t meta { fourcc( 'M', 'E', 'T', 'A' ) }; // key/value metadata
|
||||
constexpr std::uint32_t prot { fourcc( 'P', 'R', 'O', 'T' ) }; // model prototypes (module)
|
||||
constexpr std::uint32_t inst { fourcc( 'I', 'N', 'S', 'T' ) }; // lean instances (tile)
|
||||
constexpr std::uint32_t mesh { fourcc( 'M', 'E', 'S', 'H' ) }; // baked terrain mesh (tile)
|
||||
constexpr std::uint32_t shpe { fourcc( 'S', 'H', 'P', 'E' ) }; // non-terrain shapes (triangles)
|
||||
constexpr std::uint32_t line { fourcc( 'L', 'I', 'N', 'E' ) }; // line geometry nodes
|
||||
constexpr std::uint32_t incl { fourcc( 'I', 'N', 'C', 'L' ) }; // module includes (recursion refs)
|
||||
constexpr std::uint32_t plce { fourcc( 'P', 'L', 'C', 'E' ) }; // lean .inc placements (binary coords)
|
||||
constexpr std::uint32_t trst { fourcc( 'T', 'R', 'S', 'T' ) }; // trainsets
|
||||
constexpr std::uint32_t trgr { fourcc( 'T', 'R', 'G', 'R' ) }; // precomputed track graph (sim)
|
||||
constexpr std::uint32_t sidx { fourcc( 'S', 'I', 'D', 'X' ) }; // spatial section index (sim/manifest)
|
||||
// simulation record chunks (sim)
|
||||
constexpr std::uint32_t trak { fourcc( 'T', 'R', 'A', 'K' ) }; // tracks
|
||||
constexpr std::uint32_t trac { fourcc( 'T', 'R', 'A', 'C' ) }; // traction wires
|
||||
constexpr std::uint32_t pwrs { fourcc( 'P', 'W', 'R', 'S' ) }; // traction power sources
|
||||
constexpr std::uint32_t memc { fourcc( 'M', 'E', 'M', 'C' ) }; // memory cells
|
||||
constexpr std::uint32_t laun { fourcc( 'L', 'A', 'U', 'N' ) }; // event launchers
|
||||
constexpr std::uint32_t evnt { fourcc( 'E', 'V', 'N', 'T' ) }; // events
|
||||
constexpr std::uint32_t sond { fourcc( 'S', 'O', 'N', 'D' ) }; // sounds
|
||||
constexpr std::uint32_t dynm { fourcc( 'D', 'Y', 'N', 'M' ) }; // dynamic vehicles
|
||||
} // namespace chunk
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Writer side
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Little-endian byte sink. All multi-byte values are written explicitly so the
|
||||
// output does not depend on host endianness.
|
||||
class byte_writer {
|
||||
public:
|
||||
void put_u8( std::uint8_t const v ) { m_data.push_back( v ); }
|
||||
|
||||
void put_u16( std::uint16_t const v ) {
|
||||
put_u8( static_cast<std::uint8_t>( v ) );
|
||||
put_u8( static_cast<std::uint8_t>( v >> 8 ) );
|
||||
}
|
||||
|
||||
void put_u32( std::uint32_t const v ) {
|
||||
put_u16( static_cast<std::uint16_t>( v ) );
|
||||
put_u16( static_cast<std::uint16_t>( v >> 16 ) );
|
||||
}
|
||||
|
||||
void put_u64( std::uint64_t const v ) {
|
||||
put_u32( static_cast<std::uint32_t>( v ) );
|
||||
put_u32( static_cast<std::uint32_t>( v >> 32 ) );
|
||||
}
|
||||
|
||||
void put_i32( std::int32_t const v ) {
|
||||
put_u32( static_cast<std::uint32_t>( v ) );
|
||||
}
|
||||
|
||||
void put_f32( float const v ) {
|
||||
std::uint32_t bits;
|
||||
std::memcpy( &bits, &v, sizeof( bits ) );
|
||||
put_u32( bits );
|
||||
}
|
||||
|
||||
// doubles are stored as f32 on disk (positions excepted via put_f64 where
|
||||
// the extra precision actually matters)
|
||||
void put_f64( double const v ) {
|
||||
std::uint64_t bits;
|
||||
std::memcpy( &bits, &v, sizeof( bits ) );
|
||||
put_u64( bits );
|
||||
}
|
||||
|
||||
void put_vec3f( double const x, double const y, double const z ) {
|
||||
put_f32( static_cast<float>( x ) );
|
||||
put_f32( static_cast<float>( y ) );
|
||||
put_f32( static_cast<float>( z ) );
|
||||
}
|
||||
|
||||
void put_bytes( void const *data, std::size_t const size ) {
|
||||
auto const *bytes { static_cast<std::uint8_t const *>( data ) };
|
||||
m_data.insert( m_data.end(), bytes, bytes + size );
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::uint8_t> const &data() const noexcept { return m_data; }
|
||||
[[nodiscard]] std::vector<std::uint8_t> &data() noexcept { return m_data; }
|
||||
[[nodiscard]] std::size_t size() const noexcept { return m_data.size(); }
|
||||
|
||||
private:
|
||||
std::vector<std::uint8_t> m_data;
|
||||
};
|
||||
|
||||
// Deduplicating string table. Returns a stable u32 id per unique string.
|
||||
class string_table {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t intern( std::string_view const s ) {
|
||||
auto const key { std::string( s ) };
|
||||
auto const it { m_lookup.find( key ) };
|
||||
if( it != m_lookup.end() ) {
|
||||
return it->second;
|
||||
}
|
||||
auto const id { static_cast<std::uint32_t>( m_strings.size() ) };
|
||||
m_strings.emplace_back( key );
|
||||
m_lookup.emplace( key, id );
|
||||
return id;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t size() const noexcept { return m_strings.size(); }
|
||||
|
||||
// Serialise as: [u32 count]( [u32 len][bytes] )*
|
||||
void serialize( byte_writer &out ) const {
|
||||
out.put_u32( static_cast<std::uint32_t>( m_strings.size() ) );
|
||||
for( auto const &s : m_strings ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( s.size() ) );
|
||||
out.put_bytes( s.data(), s.size() );
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::string> m_strings;
|
||||
std::unordered_map<std::string, std::uint32_t> m_lookup;
|
||||
};
|
||||
|
||||
// Top-level container writer: file header followed by [id][size][payload] chunks.
|
||||
class container_writer {
|
||||
public:
|
||||
explicit container_writer( file_kind const kind ) : m_kind( kind ) {
|
||||
m_out.put_u32( kMagic );
|
||||
m_out.put_u16( kVersion );
|
||||
m_out.put_u16( static_cast<std::uint16_t>( kind ) );
|
||||
// reserved for flags / future use, keeps header 16-byte aligned
|
||||
m_out.put_u32( 0 );
|
||||
m_out.put_u32( 0 );
|
||||
}
|
||||
|
||||
// Append a fully-built chunk payload under the given id.
|
||||
void add_chunk( std::uint32_t const id, std::vector<std::uint8_t> const &payload ) {
|
||||
m_out.put_u32( id );
|
||||
m_out.put_u64( static_cast<std::uint64_t>( payload.size() ) );
|
||||
m_out.put_bytes( payload.data(), payload.size() );
|
||||
}
|
||||
|
||||
void add_chunk( std::uint32_t const id, byte_writer const &payload ) {
|
||||
add_chunk( id, payload.data() );
|
||||
}
|
||||
|
||||
[[nodiscard]] file_kind kind() const noexcept { return m_kind; }
|
||||
[[nodiscard]] std::vector<std::uint8_t> const &data() const noexcept { return m_out.data(); }
|
||||
|
||||
private:
|
||||
file_kind m_kind;
|
||||
byte_writer m_out;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reader side (bounds-checked)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class parse_error : public std::runtime_error {
|
||||
public:
|
||||
explicit parse_error( std::string const &what ) : std::runtime_error( what ) {}
|
||||
};
|
||||
|
||||
// Little-endian cursor over a byte span. Every read is bounds-checked and
|
||||
// throws parse_error on truncation so a corrupt file can never read OOB.
|
||||
class byte_reader {
|
||||
public:
|
||||
byte_reader( std::uint8_t const *data, std::size_t const size ) noexcept
|
||||
: m_data( data ), m_size( size ) {}
|
||||
|
||||
[[nodiscard]] std::size_t remaining() const noexcept { return m_size - m_pos; }
|
||||
[[nodiscard]] bool empty() const noexcept { return m_pos >= m_size; }
|
||||
[[nodiscard]] std::size_t position() const noexcept { return m_pos; }
|
||||
|
||||
std::uint8_t get_u8() {
|
||||
require( 1 );
|
||||
return m_data[ m_pos++ ];
|
||||
}
|
||||
|
||||
std::uint16_t get_u16() {
|
||||
std::uint16_t const lo { get_u8() };
|
||||
std::uint16_t const hi { get_u8() };
|
||||
return static_cast<std::uint16_t>( lo | ( hi << 8 ) );
|
||||
}
|
||||
|
||||
std::uint32_t get_u32() {
|
||||
std::uint32_t const lo { get_u16() };
|
||||
std::uint32_t const hi { get_u16() };
|
||||
return lo | ( hi << 16 );
|
||||
}
|
||||
|
||||
std::uint64_t get_u64() {
|
||||
std::uint64_t const lo { get_u32() };
|
||||
std::uint64_t const hi { get_u32() };
|
||||
return lo | ( hi << 32 );
|
||||
}
|
||||
|
||||
std::int32_t get_i32() { return static_cast<std::int32_t>( get_u32() ); }
|
||||
|
||||
float get_f32() {
|
||||
std::uint32_t const bits { get_u32() };
|
||||
float v;
|
||||
std::memcpy( &v, &bits, sizeof( v ) );
|
||||
return v;
|
||||
}
|
||||
|
||||
double get_f64() {
|
||||
std::uint64_t const bits { get_u64() };
|
||||
double v;
|
||||
std::memcpy( &v, &bits, sizeof( v ) );
|
||||
return v;
|
||||
}
|
||||
|
||||
void get_bytes( void *dst, std::size_t const size ) {
|
||||
require( size );
|
||||
std::memcpy( dst, m_data + m_pos, size );
|
||||
m_pos += size;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint8_t const *take( std::size_t const size ) {
|
||||
require( size );
|
||||
auto const *ptr { m_data + m_pos };
|
||||
m_pos += size;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
private:
|
||||
void require( std::size_t const n ) const {
|
||||
if( m_pos + n > m_size ) {
|
||||
throw parse_error( "eu7v2: unexpected end of data" );
|
||||
}
|
||||
}
|
||||
|
||||
std::uint8_t const *m_data;
|
||||
std::size_t m_size;
|
||||
std::size_t m_pos { 0 };
|
||||
};
|
||||
|
||||
// Decoded string table: id -> string view into the owned backing store.
|
||||
class string_pool {
|
||||
public:
|
||||
void deserialize( byte_reader &in ) {
|
||||
auto const count { in.get_u32() };
|
||||
m_strings.clear();
|
||||
m_strings.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
auto const len { in.get_u32() };
|
||||
std::string s;
|
||||
s.resize( len );
|
||||
if( len != 0 ) {
|
||||
in.get_bytes( s.data(), len );
|
||||
}
|
||||
m_strings.emplace_back( std::move( s ) );
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string const &get( std::uint32_t const id ) const {
|
||||
if( id >= m_strings.size() ) {
|
||||
throw parse_error( "eu7v2: string id out of range" );
|
||||
}
|
||||
return m_strings[ id ];
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t size() const noexcept { return m_strings.size(); }
|
||||
|
||||
private:
|
||||
std::vector<std::string> m_strings;
|
||||
};
|
||||
|
||||
// One chunk located inside a container: its id and a view of its payload.
|
||||
struct chunk_view {
|
||||
std::uint32_t id { 0 };
|
||||
std::uint8_t const *data { nullptr };
|
||||
std::size_t size { 0 };
|
||||
|
||||
[[nodiscard]] byte_reader reader() const noexcept { return byte_reader( data, size ); }
|
||||
};
|
||||
|
||||
// Reads the file header and iterates chunks without copying payloads.
|
||||
class container_reader {
|
||||
public:
|
||||
container_reader( std::uint8_t const *data, std::size_t const size ) : m_cursor( data, size ) {
|
||||
auto const magic { m_cursor.get_u32() };
|
||||
if( magic != kMagic ) {
|
||||
throw parse_error( "eu7v2: bad magic" );
|
||||
}
|
||||
m_version = m_cursor.get_u16();
|
||||
if( m_version != kVersion ) {
|
||||
throw parse_error( "eu7v2: unsupported version" );
|
||||
}
|
||||
m_kind = static_cast<file_kind>( m_cursor.get_u16() );
|
||||
(void)m_cursor.get_u32(); // reserved
|
||||
(void)m_cursor.get_u32(); // reserved
|
||||
}
|
||||
|
||||
[[nodiscard]] file_kind kind() const noexcept { return m_kind; }
|
||||
[[nodiscard]] std::uint16_t version() const noexcept { return m_version; }
|
||||
|
||||
// Returns false when there are no more chunks.
|
||||
[[nodiscard]] bool next( chunk_view &out ) {
|
||||
if( m_cursor.remaining() == 0 ) {
|
||||
return false;
|
||||
}
|
||||
out.id = m_cursor.get_u32();
|
||||
auto const size { m_cursor.get_u64() };
|
||||
out.size = static_cast<std::size_t>( size );
|
||||
out.data = m_cursor.take( out.size );
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
byte_reader m_cursor;
|
||||
file_kind m_kind { file_kind::sim };
|
||||
std::uint16_t m_version { 0 };
|
||||
};
|
||||
|
||||
// Maps a text scenery source to its compiled .eu7v2 path. Standard module
|
||||
// extensions (.scm/.scn/.sbt/.inc) become "<stem>.eu7v2"; other extensions
|
||||
// (.ctr, …) keep the source suffix so e.g. foo.scm and foo.ctr do not collide.
|
||||
[[nodiscard]] inline std::filesystem::path
|
||||
binary_path_from_text( std::filesystem::path const &text_path ) {
|
||||
auto const ext { text_path.extension().string() };
|
||||
if( ext == ".scm" || ext == ".scn" || ext == ".sbt" || ext == ".inc" ) {
|
||||
return text_path.parent_path() / ( text_path.stem().string() + ".eu7v2" );
|
||||
}
|
||||
return text_path.parent_path() / ( text_path.filename().string() + ".eu7v2" );
|
||||
}
|
||||
|
||||
} // namespace eu7v2
|
||||
515
scene/eu7/v2/eu7v2_load.cpp
Normal file
515
scene/eu7/v2/eu7v2_load.cpp
Normal file
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
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 "scene/eu7/v2/eu7v2_load.h"
|
||||
|
||||
#include "scene/eu7/v2/eu7v2_format.h"
|
||||
#include "scene/eu7/v2/eu7v2_scene.h"
|
||||
#include "scene/eu7/v2/eu7v2_records.h"
|
||||
#include "scene/eu7/eu7_types.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
namespace {
|
||||
|
||||
// Reverse of scene_baker: turns decoded eu7v2 structs back into Eu7Scene.
|
||||
class scene_loader {
|
||||
public:
|
||||
scene_loader( string_pool const &pool, scene::eu7::Eu7Scene &out )
|
||||
: m_pool( pool ), m_out( out ) {}
|
||||
|
||||
[[nodiscard]] std::string str( std::uint32_t const id ) const {
|
||||
return id == kNoString ? std::string() : m_pool.get( id );
|
||||
}
|
||||
|
||||
void apply_node( scene::eu7::Eu7BasicNode &n, node_record const &r ) const {
|
||||
n.name = str( r.name );
|
||||
n.node_type = str( r.type );
|
||||
n.area.center = { r.area_center.x, r.area_center.y, r.area_center.z };
|
||||
n.area.radius = r.area_radius;
|
||||
n.range_squared_min = r.range_sq_min;
|
||||
n.range_squared_max = r.range_sq_max;
|
||||
n.visible = r.visible;
|
||||
}
|
||||
|
||||
void build_models(
|
||||
std::vector<model_prototype> const &protos,
|
||||
std::vector<model_instance> const &instances ) const {
|
||||
m_out.models.reserve( instances.size() );
|
||||
for( auto const &inst : instances ) {
|
||||
if( inst.proto >= protos.size() ) {
|
||||
continue;
|
||||
}
|
||||
auto const &proto { protos[ inst.proto ] };
|
||||
scene::eu7::Eu7Model m;
|
||||
m.location = { inst.x, inst.y, inst.z };
|
||||
m.angles = { inst.ax, inst.ay, inst.az };
|
||||
m.scale = { inst.sx, inst.sy, inst.sz };
|
||||
m.model_file = str( proto.model_file );
|
||||
m.texture_file =
|
||||
inst.texture_override != kNoString ? str( inst.texture_override )
|
||||
: str( proto.texture_file );
|
||||
m.light_states = proto.light_states;
|
||||
m.light_colors = proto.light_colors;
|
||||
m.transition = ( proto.flags & proto_flag::transition ) != 0;
|
||||
m.is_terrain = ( proto.flags & proto_flag::is_terrain ) != 0;
|
||||
m.pack_flags =
|
||||
( proto.flags & proto_flag::instanceable ) ? scene::eu7::kEu7PackFlagInstanceableHint : 0u;
|
||||
m.baked_range_min = proto.range_min;
|
||||
m.baked_range_max = proto.range_max;
|
||||
m.pack_cell_id = inst.cell_id;
|
||||
if( inst.has_node ) {
|
||||
apply_node( m.node, inst.node );
|
||||
}
|
||||
else {
|
||||
m.node.name = m.model_file;
|
||||
m.node.node_type = "model";
|
||||
m.node.area.center = m.location;
|
||||
}
|
||||
m_out.models.push_back( std::move( m ) );
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_lighting(
|
||||
scene::eu7::Eu7LightingData &dst, lighting_block const &src ) {
|
||||
dst.diffuse = { src.diffuse[ 0 ], src.diffuse[ 1 ], src.diffuse[ 2 ], src.diffuse[ 3 ] };
|
||||
dst.ambient = { src.ambient[ 0 ], src.ambient[ 1 ], src.ambient[ 2 ], src.ambient[ 3 ] };
|
||||
dst.specular = { src.specular[ 0 ], src.specular[ 1 ], src.specular[ 2 ], src.specular[ 3 ] };
|
||||
}
|
||||
|
||||
void build_shapes( std::vector<shape_record> const &shapes ) const {
|
||||
m_out.shapes.reserve( shapes.size() );
|
||||
for( auto const &r : shapes ) {
|
||||
scene::eu7::Eu7Shape s;
|
||||
apply_node( s.node, r.node );
|
||||
s.translucent = r.translucent;
|
||||
s.material_path = str( r.material );
|
||||
apply_lighting( s.lighting, r.lighting );
|
||||
s.origin = { r.ox, r.oy, r.oz };
|
||||
s.vertices.reserve( r.vertices.size() );
|
||||
for( auto const &v : r.vertices ) {
|
||||
scene::eu7::Eu7WorldVertex wv;
|
||||
wv.position = {
|
||||
r.ox + static_cast<double>( v.px ),
|
||||
r.oy + static_cast<double>( v.py ),
|
||||
r.oz + static_cast<double>( v.pz ) };
|
||||
wv.normal = { v.nx, v.ny, v.nz };
|
||||
wv.u = v.u;
|
||||
wv.v = v.v;
|
||||
s.vertices.push_back( wv );
|
||||
}
|
||||
m_out.shapes.push_back( std::move( s ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_lines( std::vector<lines_record> const &items ) const {
|
||||
m_out.lines.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7Lines l;
|
||||
apply_node( l.node, r.node );
|
||||
apply_lighting( l.lighting, r.lighting );
|
||||
l.line_width = r.line_width;
|
||||
l.origin = { r.ox, r.oy, r.oz };
|
||||
l.vertices.reserve( r.vertices.size() );
|
||||
for( auto const &v : r.vertices ) {
|
||||
scene::eu7::Eu7WorldVertex wv;
|
||||
wv.position = { v.x, v.y, v.z };
|
||||
l.vertices.push_back( wv );
|
||||
}
|
||||
m_out.lines.push_back( std::move( l ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_trainsets( std::vector<trainset_record> const &items ) const {
|
||||
m_out.trainsets.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7Trainset t;
|
||||
t.name = str( r.name );
|
||||
t.track = str( r.track );
|
||||
t.offset = r.offset;
|
||||
t.velocity = r.velocity;
|
||||
for( auto const &kv : r.assignment ) {
|
||||
t.assignment.emplace( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
t.vehicle_indices.reserve( r.vehicle_indices.size() );
|
||||
for( auto const idx : r.vehicle_indices ) {
|
||||
t.vehicle_indices.push_back( static_cast<std::size_t>( idx ) );
|
||||
}
|
||||
t.couplings.reserve( r.couplings.size() );
|
||||
for( auto const c : r.couplings ) {
|
||||
t.couplings.push_back( c );
|
||||
}
|
||||
t.driver_index =
|
||||
r.driver_index == 0xffffffffu
|
||||
? static_cast<std::size_t>( -1 )
|
||||
: static_cast<std::size_t>( r.driver_index );
|
||||
m_out.trainsets.push_back( std::move( t ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_terrain( std::vector<terrain_mesh> const &meshes ) const {
|
||||
m_out.terrain_shapes.reserve( meshes.size() );
|
||||
for( auto const &mesh : meshes ) {
|
||||
scene::eu7::Eu7Shape s;
|
||||
s.material_path = str( mesh.material );
|
||||
s.translucent = mesh.translucent;
|
||||
s.origin = { mesh.ox, mesh.oy, mesh.oz };
|
||||
s.vertices.reserve( mesh.vertices.size() );
|
||||
for( auto const &v : mesh.vertices ) {
|
||||
scene::eu7::Eu7WorldVertex wv;
|
||||
wv.position = {
|
||||
mesh.ox + static_cast<double>( v.px ),
|
||||
mesh.oy + static_cast<double>( v.py ),
|
||||
mesh.oz + static_cast<double>( v.pz ) };
|
||||
wv.normal = { v.nx, v.ny, v.nz };
|
||||
wv.u = v.u;
|
||||
wv.v = v.v;
|
||||
s.vertices.push_back( wv );
|
||||
}
|
||||
m_out.terrain_shapes.push_back( std::move( s ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_tracks( std::vector<track_record> const &tracks ) const {
|
||||
m_out.tracks.reserve( tracks.size() );
|
||||
for( auto const &r : tracks ) {
|
||||
scene::eu7::Eu7Track t;
|
||||
apply_node( t.node, r.node );
|
||||
t.track_type = static_cast<scene::eu7::Eu7TrackType>( r.track_type );
|
||||
t.category = static_cast<scene::eu7::Eu7TrackCategory>( r.category );
|
||||
t.length = r.length;
|
||||
t.track_width = r.track_width;
|
||||
t.friction = r.friction;
|
||||
t.sound_distance = r.sound_distance;
|
||||
t.quality_flag = r.quality_flag;
|
||||
t.damage_flag = r.damage_flag;
|
||||
t.environment = static_cast<scene::eu7::Eu7TrackEnvironment>( r.environment );
|
||||
if( r.has_visibility ) {
|
||||
scene::eu7::Eu7TrackVisibility vis;
|
||||
vis.material1 = str( r.visibility.material1 );
|
||||
vis.tex_length = r.visibility.tex_length;
|
||||
vis.material2 = str( r.visibility.material2 );
|
||||
vis.tex_height1 = r.visibility.tex_height1;
|
||||
vis.tex_width = r.visibility.tex_width;
|
||||
vis.tex_slope = r.visibility.tex_slope;
|
||||
t.visibility = vis;
|
||||
}
|
||||
t.paths.reserve( r.paths.size() );
|
||||
for( auto const &p : r.paths ) {
|
||||
scene::eu7::Eu7SegmentPath sp;
|
||||
sp.p_start = { p.p_start.x, p.p_start.y, p.p_start.z };
|
||||
sp.roll_start = p.roll_start;
|
||||
sp.cp_out = { p.cp_out.x, p.cp_out.y, p.cp_out.z };
|
||||
sp.cp_in = { p.cp_in.x, p.cp_in.y, p.cp_in.z };
|
||||
sp.p_end = { p.p_end.x, p.p_end.y, p.p_end.z };
|
||||
sp.roll_end = p.roll_end;
|
||||
sp.radius = p.radius;
|
||||
t.paths.push_back( sp );
|
||||
}
|
||||
t.tail_keywords.reserve( r.tail_keywords.size() );
|
||||
for( auto const &kv : r.tail_keywords ) {
|
||||
t.tail_keywords.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
m_out.tracks.push_back( std::move( t ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_traction( std::vector<traction_record> const &items ) const {
|
||||
m_out.traction.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7Traction t;
|
||||
apply_node( t.node, r.node );
|
||||
t.power_supply_name = str( r.power_supply_name );
|
||||
t.nominal_voltage = r.nominal_voltage;
|
||||
t.max_current = r.max_current;
|
||||
t.resistivity_ohm_per_m = r.resistivity;
|
||||
t.material = static_cast<scene::eu7::Eu7TractionWireMaterial>( r.material );
|
||||
t.wire_thickness = r.wire_thickness;
|
||||
t.damage_flag = r.damage_flag;
|
||||
t.wire_p1 = { r.wire_p1.x, r.wire_p1.y, r.wire_p1.z };
|
||||
t.wire_p2 = { r.wire_p2.x, r.wire_p2.y, r.wire_p2.z };
|
||||
t.wire_p3 = { r.wire_p3.x, r.wire_p3.y, r.wire_p3.z };
|
||||
t.wire_p4 = { r.wire_p4.x, r.wire_p4.y, r.wire_p4.z };
|
||||
t.min_height = r.min_height;
|
||||
t.segment_length = r.segment_length;
|
||||
t.wire_count = r.wire_count;
|
||||
t.wire_offset = r.wire_offset;
|
||||
if( r.has_parallel ) {
|
||||
t.parallel_name = str( r.parallel_name );
|
||||
}
|
||||
m_out.traction.push_back( std::move( t ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_power_sources( std::vector<power_source_record> const &items ) const {
|
||||
m_out.power_sources.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7TractionPowerSource p;
|
||||
apply_node( p.node, r.node );
|
||||
p.position = { r.position.x, r.position.y, r.position.z };
|
||||
p.nominal_voltage = r.nominal_voltage;
|
||||
p.voltage_frequency = r.voltage_frequency;
|
||||
p.internal_resistance = r.internal_resistance;
|
||||
p.max_output_current = r.max_output_current;
|
||||
p.fast_fuse_timeout = r.fast_fuse_timeout;
|
||||
p.fast_fuse_repetition = r.fast_fuse_repetition;
|
||||
p.slow_fuse_timeout = r.slow_fuse_timeout;
|
||||
p.modifier = static_cast<scene::eu7::Eu7PowerSourceModifier>( r.modifier );
|
||||
m_out.power_sources.push_back( std::move( p ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_memcells( std::vector<memcell_record> const &items ) const {
|
||||
m_out.memcells.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7MemCell m;
|
||||
apply_node( m.node, r.node );
|
||||
m.text = str( r.text );
|
||||
m.value1 = r.value1;
|
||||
m.value2 = r.value2;
|
||||
if( r.has_track ) {
|
||||
m.track_name = str( r.track_name );
|
||||
}
|
||||
m_out.memcells.push_back( std::move( m ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_launchers( std::vector<launcher_record> const &items ) const {
|
||||
m_out.event_launchers.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7EventLauncher l;
|
||||
apply_node( l.node, r.node );
|
||||
l.location = { r.location.x, r.location.y, r.location.z };
|
||||
l.radius_squared = r.radius_squared;
|
||||
l.activation_key = r.activation_key;
|
||||
l.delta_time = r.delta_time;
|
||||
l.event1_name = str( r.event1_name );
|
||||
l.event2_name = str( r.event2_name );
|
||||
if( r.has_condition ) {
|
||||
scene::eu7::Eu7EventLauncherCondition c;
|
||||
c.memcell_name = str( r.condition.memcell_name );
|
||||
c.compare_text = str( r.condition.compare_text );
|
||||
c.compare_value1 = r.condition.compare_value1;
|
||||
c.compare_value2 = r.condition.compare_value2;
|
||||
c.check_mask = r.condition.check_mask;
|
||||
l.condition = c;
|
||||
}
|
||||
l.train_triggered = r.train_triggered;
|
||||
l.launch_hour = r.launch_hour;
|
||||
l.launch_minute = r.launch_minute;
|
||||
m_out.event_launchers.push_back( std::move( l ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_events( std::vector<event_record> const &items ) const {
|
||||
m_out.events.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7Event e;
|
||||
e.name = str( r.name );
|
||||
e.type = static_cast<scene::eu7::Eu7EventType>( r.type );
|
||||
e.delay = r.delay;
|
||||
e.delay_random = r.delay_random;
|
||||
e.delay_departure = r.delay_departure;
|
||||
e.ignored = r.ignored;
|
||||
e.passive = r.passive;
|
||||
e.targets.reserve( r.targets.size() );
|
||||
for( auto const t : r.targets ) {
|
||||
e.targets.push_back( str( t ) );
|
||||
}
|
||||
e.payload.reserve( r.payload.size() );
|
||||
for( auto const &kv : r.payload ) {
|
||||
e.payload.emplace_back( str( kv.first ), str( kv.second ) );
|
||||
}
|
||||
m_out.events.push_back( std::move( e ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_sounds( std::vector<sound_record> const &items ) const {
|
||||
m_out.sounds.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7Sound s;
|
||||
apply_node( s.node, r.node );
|
||||
s.location = { r.location.x, r.location.y, r.location.z };
|
||||
s.wav_file = str( r.wav_file );
|
||||
m_out.sounds.push_back( std::move( s ) );
|
||||
}
|
||||
}
|
||||
|
||||
void build_dynamics( std::vector<dynamic_record> const &items ) const {
|
||||
m_out.dynamics.reserve( items.size() );
|
||||
for( auto const &r : items ) {
|
||||
scene::eu7::Eu7Dynamic d;
|
||||
apply_node( d.node, r.node );
|
||||
d.data_folder = str( r.data_folder );
|
||||
d.skin_file = str( r.skin_file );
|
||||
d.mmd_file = str( r.mmd_file );
|
||||
d.track_name = str( r.track_name );
|
||||
d.offset = r.offset;
|
||||
d.driver_type = str( r.driver_type );
|
||||
d.coupling = r.coupling;
|
||||
d.coupling_raw = str( r.coupling_raw );
|
||||
d.coupling_params = str( r.coupling_params );
|
||||
d.velocity = r.velocity;
|
||||
d.load_count = r.load_count;
|
||||
d.load_type = str( r.load_type );
|
||||
if( r.has_destination ) {
|
||||
d.destination = str( r.destination );
|
||||
}
|
||||
if( r.has_trainset ) {
|
||||
d.trainset_index = static_cast<std::size_t>( r.trainset_index );
|
||||
}
|
||||
m_out.dynamics.push_back( std::move( d ) );
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
string_pool const &m_pool;
|
||||
scene::eu7::Eu7Scene &m_out;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
// Shared scan: reconstructs the Eu7Scene; when module != nullptr it also
|
||||
// reconstructs includes / placement / flags / first_init_count.
|
||||
[[nodiscard]] bool
|
||||
load_into(
|
||||
std::uint8_t const *data,
|
||||
std::size_t const size,
|
||||
scene::eu7::Eu7Scene &out,
|
||||
scene::eu7::Eu7Module *module ) {
|
||||
try {
|
||||
container_reader reader( data, size );
|
||||
if( reader.kind() != file_kind::sim && reader.kind() != file_kind::module ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string_pool pool;
|
||||
std::vector<model_prototype> protos;
|
||||
std::vector<model_instance> instances;
|
||||
std::vector<include_record> includes;
|
||||
std::vector<module_placement_record> placements;
|
||||
bool saw_meta { false };
|
||||
module_meta meta;
|
||||
|
||||
// STRS must be resolved before the records that reference it; the baker
|
||||
// always writes it first, so a single pass over chunks is enough.
|
||||
chunk_view chunk;
|
||||
scene_loader loader( pool, out );
|
||||
while( reader.next( chunk ) ) {
|
||||
auto r { chunk.reader() };
|
||||
switch( chunk.id ) {
|
||||
case chunk::strs: pool.deserialize( r ); break;
|
||||
case chunk::meta: meta = read_meta( r ); saw_meta = true; break;
|
||||
case chunk::incl: includes = read_includes( r ); break;
|
||||
case chunk::plce: placements = read_module_placements( r ); break;
|
||||
case chunk::prot: protos = read_prototypes( r ); break;
|
||||
case chunk::inst: instances = read_instances( r ); break;
|
||||
case chunk::mesh: loader.build_terrain( read_terrain_meshes( r ) ); break;
|
||||
case chunk::shpe: loader.build_shapes( read_shapes( r ) ); break;
|
||||
case chunk::line: loader.build_lines( read_lines( r ) ); break;
|
||||
case chunk::trak: loader.build_tracks( read_tracks( r ) ); break;
|
||||
case chunk::trac: loader.build_traction( read_traction( r ) ); break;
|
||||
case chunk::pwrs: loader.build_power_sources( read_power_sources( r ) ); break;
|
||||
case chunk::memc: loader.build_memcells( read_memcells( r ) ); break;
|
||||
case chunk::laun: loader.build_launchers( read_launchers( r ) ); break;
|
||||
case chunk::evnt: loader.build_events( read_events( r ) ); break;
|
||||
case chunk::sond: loader.build_sounds( read_sounds( r ) ); break;
|
||||
case chunk::dynm: loader.build_dynamics( read_dynamics( r ) ); break;
|
||||
case chunk::trst: loader.build_trainsets( read_trainsets( r ) ); break;
|
||||
default: break; // unknown chunk: skip
|
||||
}
|
||||
}
|
||||
|
||||
// models depend on both PROT and INST, build them after the scan
|
||||
loader.build_models( protos, instances );
|
||||
|
||||
if( saw_meta ) {
|
||||
out.first_init_count = meta.first_init_count;
|
||||
}
|
||||
|
||||
if( module != nullptr ) {
|
||||
auto resolve { [&]( std::uint32_t const id ) {
|
||||
return id == kNoString ? std::string() : pool.get( id );
|
||||
} };
|
||||
module->includes.reserve( includes.size() + placements.size() );
|
||||
for( auto const &inc : includes ) {
|
||||
scene::eu7::Eu7Include e;
|
||||
e.source_line = inc.source_line;
|
||||
e.source_path = resolve( inc.source_path );
|
||||
e.binary_path = resolve( inc.binary_path );
|
||||
e.parameters.reserve( inc.parameters.size() );
|
||||
for( auto const p : inc.parameters ) {
|
||||
e.parameters.push_back( resolve( p ) );
|
||||
}
|
||||
for( auto const &v : inc.site_transform.origin_stack ) {
|
||||
e.site_transform.origin_stack.push_back( { v.x, v.y, v.z } );
|
||||
}
|
||||
for( auto const &v : inc.site_transform.scale_stack ) {
|
||||
e.site_transform.scale_stack.push_back( { v.x, v.y, v.z } );
|
||||
}
|
||||
e.site_transform.rotation = {
|
||||
inc.site_transform.rotation.x,
|
||||
inc.site_transform.rotation.y,
|
||||
inc.site_transform.rotation.z };
|
||||
e.site_transform.group_depth =
|
||||
static_cast<std::size_t>( inc.site_transform.group_depth );
|
||||
module->includes.push_back( std::move( e ) );
|
||||
}
|
||||
for( auto const &p : placements ) {
|
||||
scene::eu7::Eu7Include e;
|
||||
e.binary_path = resolve( p.module_path );
|
||||
if( !e.binary_path.empty() ) {
|
||||
std::filesystem::path source { e.binary_path };
|
||||
source.replace_extension( ".inc" );
|
||||
e.source_path = source.generic_string();
|
||||
}
|
||||
if( p.texture_override != kNoString ) {
|
||||
e.parameters.push_back( resolve( p.texture_override ) );
|
||||
} else {
|
||||
e.parameters.push_back( "none" );
|
||||
}
|
||||
e.parameters.push_back( std::to_string( p.x ) );
|
||||
e.parameters.push_back( std::to_string( p.y ) );
|
||||
e.parameters.push_back( std::to_string( p.z ) );
|
||||
e.parameters.push_back( std::to_string( p.rotation_y ) );
|
||||
module->includes.push_back( std::move( e ) );
|
||||
}
|
||||
if( saw_meta ) {
|
||||
module->has_terrain_chunk = meta.has_terrain_chunk;
|
||||
module->has_pack_chunk = meta.has_pack_chunk;
|
||||
module->include_placement.origin_x_param = meta.placement_origin_x;
|
||||
module->include_placement.origin_y_param = meta.placement_origin_y;
|
||||
module->include_placement.origin_z_param = meta.placement_origin_z;
|
||||
module->include_placement.rotation_y_param = meta.placement_rotation_y;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch( parse_error const & ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
load_scene( std::uint8_t const *data, std::size_t const size, scene::eu7::Eu7Scene &out ) {
|
||||
return load_into( data, size, out, nullptr );
|
||||
}
|
||||
|
||||
bool
|
||||
load_module( std::uint8_t const *data, std::size_t const size, scene::eu7::Eu7Module &out ) {
|
||||
return load_into( data, size, out.scene, &out );
|
||||
}
|
||||
|
||||
} // namespace eu7v2
|
||||
36
scene/eu7/v2/eu7v2_load.h
Normal file
36
scene/eu7/v2/eu7v2_load.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace scene::eu7 {
|
||||
struct Eu7Scene;
|
||||
struct Eu7Module;
|
||||
} // namespace scene::eu7
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
// Reconstructs a parsed scene from an eu7v2 container produced by bake_scene()
|
||||
// / bake_module(). The reconstructed Eu7Scene can be fed to the existing apply
|
||||
// path, so switching to the new format only swaps serialization and reuses the
|
||||
// proven world-construction code. Returns false on a malformed container (the
|
||||
// scene is left in a partially-populated but safe state).
|
||||
[[nodiscard]] bool
|
||||
load_scene( std::uint8_t const *data, std::size_t size, scene::eu7::Eu7Scene &out );
|
||||
|
||||
// Reconstructs a full module (scene + includes + placement + flags) from an
|
||||
// eu7v2 container. The runtime needs the includes to recurse into child
|
||||
// modules. Returns false on a malformed container.
|
||||
[[nodiscard]] bool
|
||||
load_module( std::uint8_t const *data, std::size_t size, scene::eu7::Eu7Module &out );
|
||||
|
||||
} // namespace eu7v2
|
||||
816
scene/eu7/v2/eu7v2_records.h
Normal file
816
scene/eu7/v2/eu7v2_records.h
Normal file
@@ -0,0 +1,816 @@
|
||||
/*
|
||||
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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// eu7v2 simulation records (iteration 2b): the non-visual scene data that is
|
||||
// loaded once into the sim core - tracks, traction, power sources, memory
|
||||
// cells, event launchers, events, sounds and dynamic vehicles.
|
||||
//
|
||||
// All strings are referenced by string-table id; optional fields use a leading
|
||||
// presence flag so absent data costs a single byte. Dependency-free so the
|
||||
// encode/decode path stays unit testable with a standalone compiler.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include "eu7v2_format.h"
|
||||
#include "eu7v2_scene.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
// Shared helpers (put_strid/dvec3/node_record/...) live in eu7v2_scene.h so
|
||||
// scene payloads and these sim records share a single definition.
|
||||
|
||||
// --- TRAK : tracks ---------------------------------------------------------
|
||||
|
||||
struct track_path {
|
||||
dvec3 p_start;
|
||||
double roll_start { 0.0 };
|
||||
dvec3 cp_out;
|
||||
dvec3 cp_in;
|
||||
dvec3 p_end;
|
||||
double roll_end { 0.0 };
|
||||
double radius { 0.0 };
|
||||
};
|
||||
|
||||
struct track_visibility {
|
||||
std::uint32_t material1 { kNoString };
|
||||
float tex_length { 4.f };
|
||||
std::uint32_t material2 { kNoString };
|
||||
float tex_height1 { 0.f };
|
||||
float tex_width { 0.f };
|
||||
float tex_slope { 0.f };
|
||||
};
|
||||
|
||||
struct track_record {
|
||||
node_record node;
|
||||
std::uint8_t track_type { 0 };
|
||||
std::uint8_t category { 1 };
|
||||
float length { 0.f };
|
||||
float track_width { 0.f };
|
||||
float friction { 0.f };
|
||||
float sound_distance { 0.f };
|
||||
std::int32_t quality_flag { 0 };
|
||||
std::int32_t damage_flag { 0 };
|
||||
std::int8_t environment { -1 };
|
||||
bool has_visibility { false };
|
||||
track_visibility visibility;
|
||||
std::vector<track_path> paths;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> tail_keywords; // (key strid, value strid)
|
||||
};
|
||||
|
||||
inline void write_tracks( byte_writer &out, std::vector<track_record> const &tracks ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( tracks.size() ) );
|
||||
for( auto const &t : tracks ) {
|
||||
write_node( out, t.node );
|
||||
out.put_u8( t.track_type );
|
||||
out.put_u8( t.category );
|
||||
out.put_f32( t.length );
|
||||
out.put_f32( t.track_width );
|
||||
out.put_f32( t.friction );
|
||||
out.put_f32( t.sound_distance );
|
||||
out.put_i32( t.quality_flag );
|
||||
out.put_i32( t.damage_flag );
|
||||
out.put_u8( static_cast<std::uint8_t>( t.environment ) );
|
||||
out.put_u8( t.has_visibility ? 1u : 0u );
|
||||
if( t.has_visibility ) {
|
||||
out.put_u32( t.visibility.material1 );
|
||||
out.put_f32( t.visibility.tex_length );
|
||||
out.put_u32( t.visibility.material2 );
|
||||
out.put_f32( t.visibility.tex_height1 );
|
||||
out.put_f32( t.visibility.tex_width );
|
||||
out.put_f32( t.visibility.tex_slope );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( t.paths.size() ) );
|
||||
for( auto const &p : t.paths ) {
|
||||
put_dvec3( out, p.p_start.x, p.p_start.y, p.p_start.z );
|
||||
out.put_f64( p.roll_start );
|
||||
put_dvec3( out, p.cp_out.x, p.cp_out.y, p.cp_out.z );
|
||||
put_dvec3( out, p.cp_in.x, p.cp_in.y, p.cp_in.z );
|
||||
put_dvec3( out, p.p_end.x, p.p_end.y, p.p_end.z );
|
||||
out.put_f64( p.roll_end );
|
||||
out.put_f64( p.radius );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( t.tail_keywords.size() ) );
|
||||
for( auto const &kv : t.tail_keywords ) {
|
||||
out.put_u32( kv.first );
|
||||
out.put_u32( kv.second );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<track_record> read_tracks( byte_reader &in ) {
|
||||
std::vector<track_record> tracks;
|
||||
auto const count { in.get_u32() };
|
||||
tracks.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
track_record t;
|
||||
t.node = read_node( in );
|
||||
t.track_type = in.get_u8();
|
||||
t.category = in.get_u8();
|
||||
t.length = in.get_f32();
|
||||
t.track_width = in.get_f32();
|
||||
t.friction = in.get_f32();
|
||||
t.sound_distance = in.get_f32();
|
||||
t.quality_flag = in.get_i32();
|
||||
t.damage_flag = in.get_i32();
|
||||
t.environment = static_cast<std::int8_t>( in.get_u8() );
|
||||
t.has_visibility = in.get_u8() != 0;
|
||||
if( t.has_visibility ) {
|
||||
t.visibility.material1 = in.get_u32();
|
||||
t.visibility.tex_length = in.get_f32();
|
||||
t.visibility.material2 = in.get_u32();
|
||||
t.visibility.tex_height1 = in.get_f32();
|
||||
t.visibility.tex_width = in.get_f32();
|
||||
t.visibility.tex_slope = in.get_f32();
|
||||
}
|
||||
auto const paths { in.get_u32() };
|
||||
t.paths.reserve( paths );
|
||||
for( std::uint32_t p { 0 }; p < paths; ++p ) {
|
||||
track_path tp;
|
||||
tp.p_start = get_dvec3( in );
|
||||
tp.roll_start = in.get_f64();
|
||||
tp.cp_out = get_dvec3( in );
|
||||
tp.cp_in = get_dvec3( in );
|
||||
tp.p_end = get_dvec3( in );
|
||||
tp.roll_end = in.get_f64();
|
||||
tp.radius = in.get_f64();
|
||||
t.paths.push_back( tp );
|
||||
}
|
||||
auto const kws { in.get_u32() };
|
||||
t.tail_keywords.reserve( kws );
|
||||
for( std::uint32_t k { 0 }; k < kws; ++k ) {
|
||||
auto const key { in.get_u32() };
|
||||
auto const value { in.get_u32() };
|
||||
t.tail_keywords.emplace_back( key, value );
|
||||
}
|
||||
tracks.push_back( std::move( t ) );
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
// --- TRAC : traction -------------------------------------------------------
|
||||
|
||||
struct traction_record {
|
||||
node_record node;
|
||||
std::uint32_t power_supply_name { kNoString };
|
||||
float nominal_voltage { 0.f };
|
||||
float max_current { 0.f };
|
||||
float resistivity { 0.f };
|
||||
std::uint8_t material { 1 };
|
||||
float wire_thickness { 0.f };
|
||||
std::int32_t damage_flag { 0 };
|
||||
dvec3 wire_p1, wire_p2, wire_p3, wire_p4;
|
||||
double min_height { 0.0 };
|
||||
double segment_length { 0.0 };
|
||||
std::int32_t wire_count { 0 };
|
||||
float wire_offset { 0.f };
|
||||
bool has_parallel { false };
|
||||
std::uint32_t parallel_name { kNoString };
|
||||
};
|
||||
|
||||
inline void write_traction( byte_writer &out, std::vector<traction_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &t : items ) {
|
||||
write_node( out, t.node );
|
||||
out.put_u32( t.power_supply_name );
|
||||
out.put_f32( t.nominal_voltage );
|
||||
out.put_f32( t.max_current );
|
||||
out.put_f32( t.resistivity );
|
||||
out.put_u8( t.material );
|
||||
out.put_f32( t.wire_thickness );
|
||||
out.put_i32( t.damage_flag );
|
||||
put_dvec3( out, t.wire_p1.x, t.wire_p1.y, t.wire_p1.z );
|
||||
put_dvec3( out, t.wire_p2.x, t.wire_p2.y, t.wire_p2.z );
|
||||
put_dvec3( out, t.wire_p3.x, t.wire_p3.y, t.wire_p3.z );
|
||||
put_dvec3( out, t.wire_p4.x, t.wire_p4.y, t.wire_p4.z );
|
||||
out.put_f64( t.min_height );
|
||||
out.put_f64( t.segment_length );
|
||||
out.put_i32( t.wire_count );
|
||||
out.put_f32( t.wire_offset );
|
||||
put_opt_strid( out, t.has_parallel, t.parallel_name );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<traction_record> read_traction( byte_reader &in ) {
|
||||
std::vector<traction_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
traction_record t;
|
||||
t.node = read_node( in );
|
||||
t.power_supply_name = in.get_u32();
|
||||
t.nominal_voltage = in.get_f32();
|
||||
t.max_current = in.get_f32();
|
||||
t.resistivity = in.get_f32();
|
||||
t.material = in.get_u8();
|
||||
t.wire_thickness = in.get_f32();
|
||||
t.damage_flag = in.get_i32();
|
||||
t.wire_p1 = get_dvec3( in );
|
||||
t.wire_p2 = get_dvec3( in );
|
||||
t.wire_p3 = get_dvec3( in );
|
||||
t.wire_p4 = get_dvec3( in );
|
||||
t.min_height = in.get_f64();
|
||||
t.segment_length = in.get_f64();
|
||||
t.wire_count = in.get_i32();
|
||||
t.wire_offset = in.get_f32();
|
||||
t.has_parallel = in.get_u8() != 0;
|
||||
if( t.has_parallel ) {
|
||||
t.parallel_name = in.get_u32();
|
||||
}
|
||||
items.push_back( t );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- PWRS : traction power sources -----------------------------------------
|
||||
|
||||
struct power_source_record {
|
||||
node_record node;
|
||||
dvec3 position;
|
||||
float nominal_voltage { 0.f };
|
||||
float voltage_frequency { 0.f };
|
||||
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 };
|
||||
std::uint8_t modifier { 0 };
|
||||
};
|
||||
|
||||
inline void write_power_sources( byte_writer &out, std::vector<power_source_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &p : items ) {
|
||||
write_node( out, p.node );
|
||||
put_dvec3( out, p.position.x, p.position.y, p.position.z );
|
||||
out.put_f32( p.nominal_voltage );
|
||||
out.put_f32( p.voltage_frequency );
|
||||
out.put_f32( p.internal_resistance );
|
||||
out.put_f32( p.max_output_current );
|
||||
out.put_f32( p.fast_fuse_timeout );
|
||||
out.put_f32( p.fast_fuse_repetition );
|
||||
out.put_f32( p.slow_fuse_timeout );
|
||||
out.put_u8( p.modifier );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<power_source_record> read_power_sources( byte_reader &in ) {
|
||||
std::vector<power_source_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
power_source_record p;
|
||||
p.node = read_node( in );
|
||||
p.position = get_dvec3( in );
|
||||
p.nominal_voltage = in.get_f32();
|
||||
p.voltage_frequency = in.get_f32();
|
||||
p.internal_resistance = in.get_f32();
|
||||
p.max_output_current = in.get_f32();
|
||||
p.fast_fuse_timeout = in.get_f32();
|
||||
p.fast_fuse_repetition = in.get_f32();
|
||||
p.slow_fuse_timeout = in.get_f32();
|
||||
p.modifier = in.get_u8();
|
||||
items.push_back( p );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- MEMC : memory cells ---------------------------------------------------
|
||||
|
||||
struct memcell_record {
|
||||
node_record node;
|
||||
std::uint32_t text { kNoString };
|
||||
double value1 { 0.0 };
|
||||
double value2 { 0.0 };
|
||||
bool has_track { false };
|
||||
std::uint32_t track_name { kNoString };
|
||||
};
|
||||
|
||||
inline void write_memcells( byte_writer &out, std::vector<memcell_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &m : items ) {
|
||||
write_node( out, m.node );
|
||||
out.put_u32( m.text );
|
||||
out.put_f64( m.value1 );
|
||||
out.put_f64( m.value2 );
|
||||
put_opt_strid( out, m.has_track, m.track_name );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<memcell_record> read_memcells( byte_reader &in ) {
|
||||
std::vector<memcell_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
memcell_record m;
|
||||
m.node = read_node( in );
|
||||
m.text = in.get_u32();
|
||||
m.value1 = in.get_f64();
|
||||
m.value2 = in.get_f64();
|
||||
m.has_track = in.get_u8() != 0;
|
||||
if( m.has_track ) {
|
||||
m.track_name = in.get_u32();
|
||||
}
|
||||
items.push_back( m );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- LAUN : event launchers ------------------------------------------------
|
||||
|
||||
struct launcher_condition {
|
||||
std::uint32_t memcell_name { kNoString };
|
||||
std::uint32_t compare_text { kNoString };
|
||||
double compare_value1 { 0.0 };
|
||||
double compare_value2 { 0.0 };
|
||||
std::int32_t check_mask { 0 };
|
||||
};
|
||||
|
||||
struct launcher_record {
|
||||
node_record node;
|
||||
dvec3 location;
|
||||
double radius_squared { 0.0 };
|
||||
std::int32_t activation_key { 0 };
|
||||
double delta_time { -1.0 };
|
||||
std::uint32_t event1_name { kNoString };
|
||||
std::uint32_t event2_name { kNoString };
|
||||
bool has_condition { false };
|
||||
launcher_condition condition;
|
||||
bool train_triggered { false };
|
||||
std::int32_t launch_hour { -1 };
|
||||
std::int32_t launch_minute { -1 };
|
||||
};
|
||||
|
||||
inline void write_launchers( byte_writer &out, std::vector<launcher_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &l : items ) {
|
||||
write_node( out, l.node );
|
||||
put_dvec3( out, l.location.x, l.location.y, l.location.z );
|
||||
out.put_f64( l.radius_squared );
|
||||
out.put_i32( l.activation_key );
|
||||
out.put_f64( l.delta_time );
|
||||
out.put_u32( l.event1_name );
|
||||
out.put_u32( l.event2_name );
|
||||
out.put_u8( l.has_condition ? 1u : 0u );
|
||||
if( l.has_condition ) {
|
||||
out.put_u32( l.condition.memcell_name );
|
||||
out.put_u32( l.condition.compare_text );
|
||||
out.put_f64( l.condition.compare_value1 );
|
||||
out.put_f64( l.condition.compare_value2 );
|
||||
out.put_i32( l.condition.check_mask );
|
||||
}
|
||||
out.put_u8( l.train_triggered ? 1u : 0u );
|
||||
out.put_i32( l.launch_hour );
|
||||
out.put_i32( l.launch_minute );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<launcher_record> read_launchers( byte_reader &in ) {
|
||||
std::vector<launcher_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
launcher_record l;
|
||||
l.node = read_node( in );
|
||||
l.location = get_dvec3( in );
|
||||
l.radius_squared = in.get_f64();
|
||||
l.activation_key = in.get_i32();
|
||||
l.delta_time = in.get_f64();
|
||||
l.event1_name = in.get_u32();
|
||||
l.event2_name = in.get_u32();
|
||||
l.has_condition = in.get_u8() != 0;
|
||||
if( l.has_condition ) {
|
||||
l.condition.memcell_name = in.get_u32();
|
||||
l.condition.compare_text = in.get_u32();
|
||||
l.condition.compare_value1 = in.get_f64();
|
||||
l.condition.compare_value2 = in.get_f64();
|
||||
l.condition.check_mask = in.get_i32();
|
||||
}
|
||||
l.train_triggered = in.get_u8() != 0;
|
||||
l.launch_hour = in.get_i32();
|
||||
l.launch_minute = in.get_i32();
|
||||
items.push_back( l );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- EVNT : events ---------------------------------------------------------
|
||||
|
||||
struct event_record {
|
||||
std::uint32_t name { kNoString };
|
||||
std::uint8_t type { 0 };
|
||||
double delay { 0.0 };
|
||||
double delay_random { 0.0 };
|
||||
double delay_departure { 0.0 };
|
||||
bool ignored { false };
|
||||
bool passive { false };
|
||||
std::vector<std::uint32_t> targets; // string ids
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> payload; // (key strid, value strid)
|
||||
};
|
||||
|
||||
inline void write_events( byte_writer &out, std::vector<event_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &e : items ) {
|
||||
out.put_u32( e.name );
|
||||
out.put_u8( e.type );
|
||||
out.put_f64( e.delay );
|
||||
out.put_f64( e.delay_random );
|
||||
out.put_f64( e.delay_departure );
|
||||
out.put_u8( e.ignored ? 1u : 0u );
|
||||
out.put_u8( e.passive ? 1u : 0u );
|
||||
out.put_u32( static_cast<std::uint32_t>( e.targets.size() ) );
|
||||
for( auto const t : e.targets ) {
|
||||
out.put_u32( t );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( e.payload.size() ) );
|
||||
for( auto const &kv : e.payload ) {
|
||||
out.put_u32( kv.first );
|
||||
out.put_u32( kv.second );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<event_record> read_events( byte_reader &in ) {
|
||||
std::vector<event_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
event_record e;
|
||||
e.name = in.get_u32();
|
||||
e.type = in.get_u8();
|
||||
e.delay = in.get_f64();
|
||||
e.delay_random = in.get_f64();
|
||||
e.delay_departure = in.get_f64();
|
||||
e.ignored = in.get_u8() != 0;
|
||||
e.passive = in.get_u8() != 0;
|
||||
auto const targets { in.get_u32() };
|
||||
e.targets.reserve( targets );
|
||||
for( std::uint32_t t { 0 }; t < targets; ++t ) {
|
||||
e.targets.push_back( in.get_u32() );
|
||||
}
|
||||
auto const payload { in.get_u32() };
|
||||
e.payload.reserve( payload );
|
||||
for( std::uint32_t p { 0 }; p < payload; ++p ) {
|
||||
auto const key { in.get_u32() };
|
||||
auto const value { in.get_u32() };
|
||||
e.payload.emplace_back( key, value );
|
||||
}
|
||||
items.push_back( std::move( e ) );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- SOND : sounds ---------------------------------------------------------
|
||||
|
||||
struct sound_record {
|
||||
node_record node;
|
||||
dvec3 location;
|
||||
std::uint32_t wav_file { kNoString };
|
||||
};
|
||||
|
||||
inline void write_sounds( byte_writer &out, std::vector<sound_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &s : items ) {
|
||||
write_node( out, s.node );
|
||||
put_dvec3( out, s.location.x, s.location.y, s.location.z );
|
||||
out.put_u32( s.wav_file );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<sound_record> read_sounds( byte_reader &in ) {
|
||||
std::vector<sound_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
sound_record s;
|
||||
s.node = read_node( in );
|
||||
s.location = get_dvec3( in );
|
||||
s.wav_file = in.get_u32();
|
||||
items.push_back( s );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- DYNM : dynamic vehicles -----------------------------------------------
|
||||
|
||||
struct dynamic_record {
|
||||
node_record node;
|
||||
std::uint32_t data_folder { kNoString };
|
||||
std::uint32_t skin_file { kNoString };
|
||||
std::uint32_t mmd_file { kNoString };
|
||||
std::uint32_t track_name { kNoString };
|
||||
double offset { -1.0 };
|
||||
std::uint32_t driver_type { kNoString };
|
||||
std::int32_t coupling { 3 };
|
||||
std::uint32_t coupling_raw { kNoString };
|
||||
std::uint32_t coupling_params { kNoString };
|
||||
float velocity { 0.f };
|
||||
std::int32_t load_count { 0 };
|
||||
std::uint32_t load_type { kNoString };
|
||||
bool has_destination { false };
|
||||
std::uint32_t destination { kNoString };
|
||||
bool has_trainset { false };
|
||||
std::uint32_t trainset_index { 0 };
|
||||
};
|
||||
|
||||
inline void write_dynamics( byte_writer &out, std::vector<dynamic_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &d : items ) {
|
||||
write_node( out, d.node );
|
||||
out.put_u32( d.data_folder );
|
||||
out.put_u32( d.skin_file );
|
||||
out.put_u32( d.mmd_file );
|
||||
out.put_u32( d.track_name );
|
||||
out.put_f64( d.offset );
|
||||
out.put_u32( d.driver_type );
|
||||
out.put_i32( d.coupling );
|
||||
out.put_u32( d.coupling_raw );
|
||||
out.put_u32( d.coupling_params );
|
||||
out.put_f32( d.velocity );
|
||||
out.put_i32( d.load_count );
|
||||
out.put_u32( d.load_type );
|
||||
put_opt_strid( out, d.has_destination, d.destination );
|
||||
out.put_u8( d.has_trainset ? 1u : 0u );
|
||||
if( d.has_trainset ) {
|
||||
out.put_u32( d.trainset_index );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<dynamic_record> read_dynamics( byte_reader &in ) {
|
||||
std::vector<dynamic_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
dynamic_record d;
|
||||
d.node = read_node( in );
|
||||
d.data_folder = in.get_u32();
|
||||
d.skin_file = in.get_u32();
|
||||
d.mmd_file = in.get_u32();
|
||||
d.track_name = in.get_u32();
|
||||
d.offset = in.get_f64();
|
||||
d.driver_type = in.get_u32();
|
||||
d.coupling = in.get_i32();
|
||||
d.coupling_raw = in.get_u32();
|
||||
d.coupling_params = in.get_u32();
|
||||
d.velocity = in.get_f32();
|
||||
d.load_count = in.get_i32();
|
||||
d.load_type = in.get_u32();
|
||||
d.has_destination = in.get_u8() != 0;
|
||||
if( d.has_destination ) {
|
||||
d.destination = in.get_u32();
|
||||
}
|
||||
d.has_trainset = in.get_u8() != 0;
|
||||
if( d.has_trainset ) {
|
||||
d.trainset_index = in.get_u32();
|
||||
}
|
||||
items.push_back( d );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- TRST : trainsets ------------------------------------------------------
|
||||
|
||||
struct trainset_record {
|
||||
std::uint32_t name { kNoString };
|
||||
std::uint32_t track { kNoString };
|
||||
float offset { 0.f };
|
||||
float velocity { 0.f };
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> assignment; // (key strid, value strid)
|
||||
std::vector<std::uint32_t> vehicle_indices;
|
||||
std::vector<std::int32_t> couplings;
|
||||
std::uint32_t driver_index { 0xffffffffu }; // (size_t)-1 sentinel
|
||||
};
|
||||
|
||||
inline void write_trainsets( byte_writer &out, std::vector<trainset_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &t : items ) {
|
||||
out.put_u32( t.name );
|
||||
out.put_u32( t.track );
|
||||
out.put_f32( t.offset );
|
||||
out.put_f32( t.velocity );
|
||||
out.put_u32( static_cast<std::uint32_t>( t.assignment.size() ) );
|
||||
for( auto const &kv : t.assignment ) {
|
||||
out.put_u32( kv.first );
|
||||
out.put_u32( kv.second );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( t.vehicle_indices.size() ) );
|
||||
for( auto const idx : t.vehicle_indices ) {
|
||||
out.put_u32( idx );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( t.couplings.size() ) );
|
||||
for( auto const c : t.couplings ) {
|
||||
out.put_i32( c );
|
||||
}
|
||||
out.put_u32( t.driver_index );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<trainset_record> read_trainsets( byte_reader &in ) {
|
||||
std::vector<trainset_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
trainset_record t;
|
||||
t.name = in.get_u32();
|
||||
t.track = in.get_u32();
|
||||
t.offset = in.get_f32();
|
||||
t.velocity = in.get_f32();
|
||||
auto const assign { in.get_u32() };
|
||||
t.assignment.reserve( assign );
|
||||
for( std::uint32_t a { 0 }; a < assign; ++a ) {
|
||||
auto const key { in.get_u32() };
|
||||
auto const value { in.get_u32() };
|
||||
t.assignment.emplace_back( key, value );
|
||||
}
|
||||
auto const vehicles { in.get_u32() };
|
||||
t.vehicle_indices.reserve( vehicles );
|
||||
for( std::uint32_t v { 0 }; v < vehicles; ++v ) {
|
||||
t.vehicle_indices.push_back( in.get_u32() );
|
||||
}
|
||||
auto const couplings { in.get_u32() };
|
||||
t.couplings.reserve( couplings );
|
||||
for( std::uint32_t c { 0 }; c < couplings; ++c ) {
|
||||
t.couplings.push_back( in.get_i32() );
|
||||
}
|
||||
t.driver_index = in.get_u32();
|
||||
items.push_back( std::move( t ) );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- PLCE : lean reusable-module placements (.inc with x/y/z/rot as f64/f32) -
|
||||
|
||||
struct module_placement_record {
|
||||
std::uint32_t module_path { kNoString }; // e.g. scenery/grass_l61/20.eu7v2
|
||||
std::uint32_t texture_override { kNoString };
|
||||
double x { 0.0 }, y { 0.0 }, z { 0.0 };
|
||||
float rotation_y { 0.f };
|
||||
std::uint8_t cell_id { 0xffu };
|
||||
};
|
||||
|
||||
inline void write_module_placements(
|
||||
byte_writer &out,
|
||||
std::vector<module_placement_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &p : items ) {
|
||||
out.put_u32( p.module_path );
|
||||
out.put_u32( p.texture_override );
|
||||
out.put_f64( p.x );
|
||||
out.put_f64( p.y );
|
||||
out.put_f64( p.z );
|
||||
out.put_f32( p.rotation_y );
|
||||
out.put_u8( p.cell_id );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<module_placement_record> read_module_placements( byte_reader &in ) {
|
||||
std::vector<module_placement_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
module_placement_record p;
|
||||
p.module_path = in.get_u32();
|
||||
p.texture_override = in.get_u32();
|
||||
p.x = in.get_f64();
|
||||
p.y = in.get_f64();
|
||||
p.z = in.get_f64();
|
||||
p.rotation_y = in.get_f32();
|
||||
p.cell_id = in.get_u8();
|
||||
items.push_back( std::move( p ) );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- INCL : module includes (recursion references) -------------------------
|
||||
|
||||
// Snapshot of the origin/scale/rotation stacks at include site (detokenizer).
|
||||
struct transform_record {
|
||||
std::vector<dvec3> origin_stack;
|
||||
std::vector<dvec3> scale_stack;
|
||||
dvec3 rotation;
|
||||
std::uint32_t group_depth { 0 };
|
||||
};
|
||||
|
||||
inline void write_transform( byte_writer &out, transform_record const &t ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( t.origin_stack.size() ) );
|
||||
for( auto const &v : t.origin_stack ) {
|
||||
put_dvec3( out, v.x, v.y, v.z );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( t.scale_stack.size() ) );
|
||||
for( auto const &v : t.scale_stack ) {
|
||||
put_dvec3( out, v.x, v.y, v.z );
|
||||
}
|
||||
put_dvec3( out, t.rotation.x, t.rotation.y, t.rotation.z );
|
||||
out.put_u32( t.group_depth );
|
||||
}
|
||||
|
||||
inline transform_record read_transform( byte_reader &in ) {
|
||||
transform_record t;
|
||||
auto const origins { in.get_u32() };
|
||||
t.origin_stack.reserve( origins );
|
||||
for( std::uint32_t i { 0 }; i < origins; ++i ) {
|
||||
t.origin_stack.push_back( get_dvec3( in ) );
|
||||
}
|
||||
auto const scales { in.get_u32() };
|
||||
t.scale_stack.reserve( scales );
|
||||
for( std::uint32_t i { 0 }; i < scales; ++i ) {
|
||||
t.scale_stack.push_back( get_dvec3( in ) );
|
||||
}
|
||||
t.rotation = get_dvec3( in );
|
||||
t.group_depth = in.get_u32();
|
||||
return t;
|
||||
}
|
||||
|
||||
struct include_record {
|
||||
std::uint32_t source_line { 0 };
|
||||
std::uint32_t source_path { kNoString };
|
||||
std::uint32_t binary_path { kNoString }; // points at the .eu7v2 module
|
||||
std::vector<std::uint32_t> parameters; // string ids
|
||||
transform_record site_transform;
|
||||
};
|
||||
|
||||
inline void write_includes( byte_writer &out, std::vector<include_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &inc : items ) {
|
||||
out.put_u32( inc.source_line );
|
||||
out.put_u32( inc.source_path );
|
||||
out.put_u32( inc.binary_path );
|
||||
out.put_u32( static_cast<std::uint32_t>( inc.parameters.size() ) );
|
||||
for( auto const p : inc.parameters ) {
|
||||
out.put_u32( p );
|
||||
}
|
||||
write_transform( out, inc.site_transform );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<include_record> read_includes( byte_reader &in ) {
|
||||
std::vector<include_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
include_record inc;
|
||||
inc.source_line = in.get_u32();
|
||||
inc.source_path = in.get_u32();
|
||||
inc.binary_path = in.get_u32();
|
||||
auto const params { in.get_u32() };
|
||||
inc.parameters.reserve( params );
|
||||
for( std::uint32_t p { 0 }; p < params; ++p ) {
|
||||
inc.parameters.push_back( in.get_u32() );
|
||||
}
|
||||
inc.site_transform = read_transform( in );
|
||||
items.push_back( std::move( inc ) );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// --- META : module-level metadata (flags, placement, counts) ---------------
|
||||
|
||||
struct module_meta {
|
||||
std::uint32_t first_init_count { 0 };
|
||||
bool has_terrain_chunk { false };
|
||||
bool has_pack_chunk { false };
|
||||
// include placement (parameter indices, 0 = unused)
|
||||
std::uint8_t placement_origin_x { 0 };
|
||||
std::uint8_t placement_origin_y { 0 };
|
||||
std::uint8_t placement_origin_z { 0 };
|
||||
std::uint8_t placement_rotation_y { 0 };
|
||||
};
|
||||
|
||||
inline void write_meta( byte_writer &out, module_meta const &m ) {
|
||||
out.put_u32( 1u ); // meta layout version (forward-tolerant)
|
||||
out.put_u32( m.first_init_count );
|
||||
out.put_u8( m.has_terrain_chunk ? 1u : 0u );
|
||||
out.put_u8( m.has_pack_chunk ? 1u : 0u );
|
||||
out.put_u8( m.placement_origin_x );
|
||||
out.put_u8( m.placement_origin_y );
|
||||
out.put_u8( m.placement_origin_z );
|
||||
out.put_u8( m.placement_rotation_y );
|
||||
}
|
||||
|
||||
inline module_meta read_meta( byte_reader &in ) {
|
||||
module_meta m;
|
||||
(void)in.get_u32(); // layout version
|
||||
m.first_init_count = in.get_u32();
|
||||
m.has_terrain_chunk = in.get_u8() != 0;
|
||||
m.has_pack_chunk = in.get_u8() != 0;
|
||||
m.placement_origin_x = in.get_u8();
|
||||
m.placement_origin_y = in.get_u8();
|
||||
m.placement_origin_z = in.get_u8();
|
||||
m.placement_rotation_y = in.get_u8();
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace eu7v2
|
||||
472
scene/eu7/v2/eu7v2_scene.h
Normal file
472
scene/eu7/v2/eu7v2_scene.h
Normal file
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// eu7v2 scene payloads (iteration 2 slice): the lean model + terrain records
|
||||
// that fix the "heavy per-object" problem of the legacy streaming path.
|
||||
//
|
||||
// PROT - deduplicated model prototypes (mesh + material + LOD + lights),
|
||||
// stored once per unique model, referenced by index.
|
||||
// INST - lean instance: prototype index + world transform + optional
|
||||
// per-instance texture override. No engine object is created here;
|
||||
// the runtime resolves prototype -> shared mesh and renders via
|
||||
// instancing.
|
||||
// MESH - baked terrain mesh: origin (f64) + vertices relative to origin
|
||||
// (f32) so a 1km tile keeps precision without paying f64 per vertex.
|
||||
//
|
||||
// Dependency-free on purpose so the whole encode/decode path is unit testable
|
||||
// with a standalone compiler (see eu7v2_test.cpp), independent of the engine.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include "eu7v2_format.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace eu7v2 {
|
||||
|
||||
constexpr std::uint32_t kNoString { 0xffffffffu };
|
||||
|
||||
// --- shared serialization helpers (used by scene payloads and sim records) -
|
||||
|
||||
inline void put_strid( byte_writer &out, std::uint32_t const id ) { out.put_u32( id ); }
|
||||
inline std::uint32_t get_strid( byte_reader &in ) { return in.get_u32(); }
|
||||
|
||||
inline void put_dvec3( byte_writer &out, double const x, double const y, double const z ) {
|
||||
out.put_f64( x );
|
||||
out.put_f64( y );
|
||||
out.put_f64( z );
|
||||
}
|
||||
|
||||
struct dvec3 {
|
||||
double x { 0.0 }, y { 0.0 }, z { 0.0 };
|
||||
};
|
||||
|
||||
inline dvec3 get_dvec3( byte_reader &in ) {
|
||||
dvec3 v;
|
||||
v.x = in.get_f64();
|
||||
v.y = in.get_f64();
|
||||
v.z = in.get_f64();
|
||||
return v;
|
||||
}
|
||||
|
||||
inline void put_opt_strid( byte_writer &out, bool const present, std::uint32_t const id ) {
|
||||
out.put_u8( present ? 1u : 0u );
|
||||
if( present ) {
|
||||
out.put_u32( id );
|
||||
}
|
||||
}
|
||||
|
||||
// Common node metadata kept after baking (world-space, transform already applied).
|
||||
struct node_record {
|
||||
std::uint32_t name { kNoString };
|
||||
std::uint32_t type { kNoString };
|
||||
dvec3 area_center;
|
||||
float area_radius { -1.f };
|
||||
double range_sq_min { 0.0 };
|
||||
double range_sq_max { 0.0 };
|
||||
bool visible { true };
|
||||
};
|
||||
|
||||
inline void write_node( byte_writer &out, node_record const &n ) {
|
||||
out.put_u32( n.name );
|
||||
out.put_u32( n.type );
|
||||
put_dvec3( out, n.area_center.x, n.area_center.y, n.area_center.z );
|
||||
out.put_f32( n.area_radius );
|
||||
out.put_f64( n.range_sq_min );
|
||||
out.put_f64( n.range_sq_max );
|
||||
out.put_u8( n.visible ? 1u : 0u );
|
||||
}
|
||||
|
||||
inline node_record read_node( byte_reader &in ) {
|
||||
node_record n;
|
||||
n.name = in.get_u32();
|
||||
n.type = in.get_u32();
|
||||
n.area_center = get_dvec3( in );
|
||||
n.area_radius = in.get_f32();
|
||||
n.range_sq_min = in.get_f64();
|
||||
n.range_sq_max = in.get_f64();
|
||||
n.visible = in.get_u8() != 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
// RGBA-ish lighting block stored as 12 floats (diffuse/ambient/specular vec4).
|
||||
struct lighting_block {
|
||||
float diffuse[ 4 ] { 0.8f, 0.8f, 0.8f, 1.f };
|
||||
float ambient[ 4 ] { 0.2f, 0.2f, 0.2f, 1.f };
|
||||
float specular[ 4 ] { 0.f, 0.f, 0.f, 1.f };
|
||||
};
|
||||
|
||||
inline void write_lighting( byte_writer &out, lighting_block const &l ) {
|
||||
for( auto const v : l.diffuse ) { out.put_f32( v ); }
|
||||
for( auto const v : l.ambient ) { out.put_f32( v ); }
|
||||
for( auto const v : l.specular ) { out.put_f32( v ); }
|
||||
}
|
||||
|
||||
inline lighting_block read_lighting( byte_reader &in ) {
|
||||
lighting_block l;
|
||||
for( auto &v : l.diffuse ) { v = in.get_f32(); }
|
||||
for( auto &v : l.ambient ) { v = in.get_f32(); }
|
||||
for( auto &v : l.specular ) { v = in.get_f32(); }
|
||||
return l;
|
||||
}
|
||||
|
||||
// Prototype flags packed into a single byte.
|
||||
namespace proto_flag {
|
||||
constexpr std::uint8_t transition { 1u << 0 }; // model has LOD transition
|
||||
constexpr std::uint8_t is_terrain { 1u << 1 }; // terrain-style submodel split
|
||||
constexpr std::uint8_t instanceable { 1u << 2 }; // safe to GPU-instance
|
||||
} // namespace proto_flag
|
||||
|
||||
struct model_prototype {
|
||||
std::uint32_t model_file { kNoString }; // string id
|
||||
std::uint32_t texture_file { kNoString }; // string id (default skin)
|
||||
std::uint8_t flags { 0 };
|
||||
float range_min { -1.f };
|
||||
float range_max { -1.f };
|
||||
std::vector<float> light_states;
|
||||
std::vector<std::uint32_t> light_colors;
|
||||
};
|
||||
|
||||
// Lean instance: which prototype, where, and an optional skin override.
|
||||
struct model_instance {
|
||||
std::uint32_t proto { 0 };
|
||||
double x { 0.0 }, y { 0.0 }, z { 0.0 };
|
||||
float ax { 0.f }, ay { 0.f }, az { 0.f }; // euler angles (deg), engine convention
|
||||
float sx { 1.f }, sy { 1.f }, sz { 1.f }; // scale
|
||||
std::uint32_t texture_override { kNoString };
|
||||
std::uint8_t cell_id { 0xffu };
|
||||
bool has_node { false }; // full node metadata present (lossless path)
|
||||
node_record node;
|
||||
};
|
||||
|
||||
struct mesh_vertex {
|
||||
float px { 0.f }, py { 0.f }, pz { 0.f }; // position relative to mesh origin
|
||||
float nx { 0.f }, ny { 0.f }, nz { 0.f }; // normal
|
||||
float u { 0.f }, v { 0.f }; // texcoord
|
||||
};
|
||||
|
||||
struct terrain_mesh {
|
||||
std::uint32_t material { kNoString }; // string id
|
||||
bool translucent { false };
|
||||
double ox { 0.0 }, oy { 0.0 }, oz { 0.0 }; // world origin (f64)
|
||||
std::vector<mesh_vertex> vertices;
|
||||
};
|
||||
|
||||
// --- PROT ------------------------------------------------------------------
|
||||
|
||||
inline void
|
||||
write_prototypes( byte_writer &out, std::vector<model_prototype> const &protos ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( protos.size() ) );
|
||||
for( auto const &p : protos ) {
|
||||
out.put_u32( p.model_file );
|
||||
out.put_u32( p.texture_file );
|
||||
out.put_u8( p.flags );
|
||||
out.put_f32( p.range_min );
|
||||
out.put_f32( p.range_max );
|
||||
out.put_u32( static_cast<std::uint32_t>( p.light_states.size() ) );
|
||||
for( auto const s : p.light_states ) {
|
||||
out.put_f32( s );
|
||||
}
|
||||
out.put_u32( static_cast<std::uint32_t>( p.light_colors.size() ) );
|
||||
for( auto const c : p.light_colors ) {
|
||||
out.put_u32( c );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<model_prototype>
|
||||
read_prototypes( byte_reader &in ) {
|
||||
std::vector<model_prototype> protos;
|
||||
auto const count { in.get_u32() };
|
||||
protos.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
model_prototype p;
|
||||
p.model_file = in.get_u32();
|
||||
p.texture_file = in.get_u32();
|
||||
p.flags = in.get_u8();
|
||||
p.range_min = in.get_f32();
|
||||
p.range_max = in.get_f32();
|
||||
auto const states { in.get_u32() };
|
||||
p.light_states.reserve( states );
|
||||
for( std::uint32_t s { 0 }; s < states; ++s ) {
|
||||
p.light_states.push_back( in.get_f32() );
|
||||
}
|
||||
auto const colors { in.get_u32() };
|
||||
p.light_colors.reserve( colors );
|
||||
for( std::uint32_t c { 0 }; c < colors; ++c ) {
|
||||
p.light_colors.push_back( in.get_u32() );
|
||||
}
|
||||
protos.push_back( std::move( p ) );
|
||||
}
|
||||
return protos;
|
||||
}
|
||||
|
||||
// --- INST ------------------------------------------------------------------
|
||||
|
||||
// Per-instance presence flags so common cases (unit scale, no override) stay
|
||||
// compact instead of always paying for scale + override.
|
||||
namespace inst_flag {
|
||||
constexpr std::uint8_t has_scale { 1u << 0 };
|
||||
constexpr std::uint8_t has_texture_override { 1u << 1 };
|
||||
constexpr std::uint8_t has_node { 1u << 2 };
|
||||
} // namespace inst_flag
|
||||
|
||||
inline void
|
||||
write_instances( byte_writer &out, std::vector<model_instance> const &instances ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( instances.size() ) );
|
||||
for( auto const &i : instances ) {
|
||||
std::uint8_t flags { 0 };
|
||||
bool const unit_scale { i.sx == 1.f && i.sy == 1.f && i.sz == 1.f };
|
||||
if( !unit_scale ) {
|
||||
flags |= inst_flag::has_scale;
|
||||
}
|
||||
if( i.texture_override != kNoString ) {
|
||||
flags |= inst_flag::has_texture_override;
|
||||
}
|
||||
if( i.has_node ) {
|
||||
flags |= inst_flag::has_node;
|
||||
}
|
||||
out.put_u8( flags );
|
||||
out.put_u32( i.proto );
|
||||
out.put_f64( i.x );
|
||||
out.put_f64( i.y );
|
||||
out.put_f64( i.z );
|
||||
out.put_f32( i.ax );
|
||||
out.put_f32( i.ay );
|
||||
out.put_f32( i.az );
|
||||
out.put_u8( i.cell_id );
|
||||
if( flags & inst_flag::has_scale ) {
|
||||
out.put_f32( i.sx );
|
||||
out.put_f32( i.sy );
|
||||
out.put_f32( i.sz );
|
||||
}
|
||||
if( flags & inst_flag::has_texture_override ) {
|
||||
out.put_u32( i.texture_override );
|
||||
}
|
||||
if( flags & inst_flag::has_node ) {
|
||||
write_node( out, i.node );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<model_instance>
|
||||
read_instances( byte_reader &in ) {
|
||||
std::vector<model_instance> instances;
|
||||
auto const count { in.get_u32() };
|
||||
instances.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
model_instance m;
|
||||
auto const flags { in.get_u8() };
|
||||
m.proto = in.get_u32();
|
||||
m.x = in.get_f64();
|
||||
m.y = in.get_f64();
|
||||
m.z = in.get_f64();
|
||||
m.ax = in.get_f32();
|
||||
m.ay = in.get_f32();
|
||||
m.az = in.get_f32();
|
||||
m.cell_id = in.get_u8();
|
||||
if( flags & inst_flag::has_scale ) {
|
||||
m.sx = in.get_f32();
|
||||
m.sy = in.get_f32();
|
||||
m.sz = in.get_f32();
|
||||
}
|
||||
if( flags & inst_flag::has_texture_override ) {
|
||||
m.texture_override = in.get_u32();
|
||||
}
|
||||
if( flags & inst_flag::has_node ) {
|
||||
m.has_node = true;
|
||||
m.node = read_node( in );
|
||||
}
|
||||
instances.push_back( m );
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
// --- MESH ------------------------------------------------------------------
|
||||
|
||||
inline void
|
||||
write_terrain_meshes( byte_writer &out, std::vector<terrain_mesh> const &meshes ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( meshes.size() ) );
|
||||
for( auto const &m : meshes ) {
|
||||
out.put_u32( m.material );
|
||||
out.put_u8( m.translucent ? 1u : 0u );
|
||||
out.put_f64( m.ox );
|
||||
out.put_f64( m.oy );
|
||||
out.put_f64( m.oz );
|
||||
out.put_u32( static_cast<std::uint32_t>( m.vertices.size() ) );
|
||||
for( auto const &v : m.vertices ) {
|
||||
out.put_f32( v.px );
|
||||
out.put_f32( v.py );
|
||||
out.put_f32( v.pz );
|
||||
out.put_f32( v.nx );
|
||||
out.put_f32( v.ny );
|
||||
out.put_f32( v.nz );
|
||||
out.put_f32( v.u );
|
||||
out.put_f32( v.v );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<terrain_mesh>
|
||||
read_terrain_meshes( byte_reader &in ) {
|
||||
std::vector<terrain_mesh> meshes;
|
||||
auto const count { in.get_u32() };
|
||||
meshes.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
terrain_mesh m;
|
||||
m.material = in.get_u32();
|
||||
m.translucent = in.get_u8() != 0;
|
||||
m.ox = in.get_f64();
|
||||
m.oy = in.get_f64();
|
||||
m.oz = in.get_f64();
|
||||
auto const verts { in.get_u32() };
|
||||
m.vertices.reserve( verts );
|
||||
for( std::uint32_t v { 0 }; v < verts; ++v ) {
|
||||
mesh_vertex mv;
|
||||
mv.px = in.get_f32();
|
||||
mv.py = in.get_f32();
|
||||
mv.pz = in.get_f32();
|
||||
mv.nx = in.get_f32();
|
||||
mv.ny = in.get_f32();
|
||||
mv.nz = in.get_f32();
|
||||
mv.u = in.get_f32();
|
||||
mv.v = in.get_f32();
|
||||
m.vertices.push_back( mv );
|
||||
}
|
||||
meshes.push_back( std::move( m ) );
|
||||
}
|
||||
return meshes;
|
||||
}
|
||||
|
||||
// --- SHPE : non-terrain shape nodes (triangles/strip/fan, baked world-space) -
|
||||
// Lossless counterpart of MESH: keeps node metadata, lighting and translucency.
|
||||
// Vertex positions are stored relative to origin (f64) as f32, matching MESH.
|
||||
|
||||
struct shape_record {
|
||||
node_record node;
|
||||
bool translucent { false };
|
||||
std::uint32_t material { kNoString };
|
||||
lighting_block lighting;
|
||||
double ox { 0.0 }, oy { 0.0 }, oz { 0.0 };
|
||||
std::vector<mesh_vertex> vertices;
|
||||
};
|
||||
|
||||
inline void write_shape_record( byte_writer &out, shape_record const &s ) {
|
||||
write_node( out, s.node );
|
||||
out.put_u8( s.translucent ? 1u : 0u );
|
||||
out.put_u32( s.material );
|
||||
write_lighting( out, s.lighting );
|
||||
out.put_f64( s.ox );
|
||||
out.put_f64( s.oy );
|
||||
out.put_f64( s.oz );
|
||||
out.put_u32( static_cast<std::uint32_t>( s.vertices.size() ) );
|
||||
for( auto const &v : s.vertices ) {
|
||||
out.put_f32( v.px );
|
||||
out.put_f32( v.py );
|
||||
out.put_f32( v.pz );
|
||||
out.put_f32( v.nx );
|
||||
out.put_f32( v.ny );
|
||||
out.put_f32( v.nz );
|
||||
out.put_f32( v.u );
|
||||
out.put_f32( v.v );
|
||||
}
|
||||
}
|
||||
|
||||
inline void write_shapes( byte_writer &out, std::vector<shape_record> const &shapes ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( shapes.size() ) );
|
||||
for( auto const &s : shapes ) {
|
||||
write_shape_record( out, s );
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<shape_record> read_shapes( byte_reader &in ) {
|
||||
std::vector<shape_record> shapes;
|
||||
auto const count { in.get_u32() };
|
||||
shapes.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
shape_record s;
|
||||
s.node = read_node( in );
|
||||
s.translucent = in.get_u8() != 0;
|
||||
s.material = in.get_u32();
|
||||
s.lighting = read_lighting( in );
|
||||
s.ox = in.get_f64();
|
||||
s.oy = in.get_f64();
|
||||
s.oz = in.get_f64();
|
||||
auto const verts { in.get_u32() };
|
||||
s.vertices.reserve( verts );
|
||||
for( std::uint32_t v { 0 }; v < verts; ++v ) {
|
||||
mesh_vertex mv;
|
||||
mv.px = in.get_f32();
|
||||
mv.py = in.get_f32();
|
||||
mv.pz = in.get_f32();
|
||||
mv.nx = in.get_f32();
|
||||
mv.ny = in.get_f32();
|
||||
mv.nz = in.get_f32();
|
||||
mv.u = in.get_f32();
|
||||
mv.v = in.get_f32();
|
||||
s.vertices.push_back( mv );
|
||||
}
|
||||
shapes.push_back( std::move( s ) );
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
// --- LINE : line geometry nodes (only vertex positions are meaningful) -------
|
||||
|
||||
struct lines_record {
|
||||
node_record node;
|
||||
lighting_block lighting;
|
||||
float line_width { 1.f };
|
||||
double ox { 0.0 }, oy { 0.0 }, oz { 0.0 };
|
||||
std::vector<dvec3> vertices; // world-space positions
|
||||
};
|
||||
|
||||
inline void write_lines( byte_writer &out, std::vector<lines_record> const &items ) {
|
||||
out.put_u32( static_cast<std::uint32_t>( items.size() ) );
|
||||
for( auto const &l : items ) {
|
||||
write_node( out, l.node );
|
||||
write_lighting( out, l.lighting );
|
||||
out.put_f32( l.line_width );
|
||||
out.put_f64( l.ox );
|
||||
out.put_f64( l.oy );
|
||||
out.put_f64( l.oz );
|
||||
out.put_u32( static_cast<std::uint32_t>( l.vertices.size() ) );
|
||||
for( auto const &v : l.vertices ) {
|
||||
put_dvec3( out, v.x, v.y, v.z );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline std::vector<lines_record> read_lines( byte_reader &in ) {
|
||||
std::vector<lines_record> items;
|
||||
auto const count { in.get_u32() };
|
||||
items.reserve( count );
|
||||
for( std::uint32_t i { 0 }; i < count; ++i ) {
|
||||
lines_record l;
|
||||
l.node = read_node( in );
|
||||
l.lighting = read_lighting( in );
|
||||
l.line_width = in.get_f32();
|
||||
l.ox = in.get_f64();
|
||||
l.oy = in.get_f64();
|
||||
l.oz = in.get_f64();
|
||||
auto const verts { in.get_u32() };
|
||||
l.vertices.reserve( verts );
|
||||
for( std::uint32_t v { 0 }; v < verts; ++v ) {
|
||||
l.vertices.push_back( get_dvec3( in ) );
|
||||
}
|
||||
items.push_back( std::move( l ) );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
} // namespace eu7v2
|
||||
673
scene/eu7/v2/eu7v2_test.cpp
Normal file
673
scene/eu7/v2/eu7v2_test.cpp
Normal file
@@ -0,0 +1,673 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
|
||||
// Standalone round-trip test for the eu7v2 format core. Builds in isolation
|
||||
// (no engine dependencies) so the foundation can be validated without running
|
||||
// the simulator:
|
||||
// g++ -std=c++20 scene/eu7/v2/eu7v2_test.cpp -o eu7v2_test && ./eu7v2_test
|
||||
|
||||
#include "eu7v2_format.h"
|
||||
#include "eu7v2_scene.h"
|
||||
#include "eu7v2_records.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace eu7v2;
|
||||
|
||||
int g_checks { 0 };
|
||||
int g_failures { 0 };
|
||||
|
||||
void
|
||||
check( bool const ok, char const *what ) {
|
||||
++g_checks;
|
||||
if( !ok ) {
|
||||
++g_failures;
|
||||
std::printf( " FAIL: %s\n", what );
|
||||
}
|
||||
}
|
||||
|
||||
// A lean instance record: prototype id + world transform + optional texture override.
|
||||
struct sample_instance {
|
||||
std::uint32_t proto_id;
|
||||
double x, y, z;
|
||||
float yaw;
|
||||
std::uint32_t texture_override; // string id, 0xffffffff = none
|
||||
};
|
||||
|
||||
// A precomputed track-graph edge: track index -> neighbour at an endpoint.
|
||||
struct sample_edge {
|
||||
std::uint32_t track;
|
||||
std::uint32_t neighbour;
|
||||
std::uint8_t end; // 0 = prev, 1 = next
|
||||
std::uint8_t neighbour_end;
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t>
|
||||
build_tile() {
|
||||
string_table strings;
|
||||
auto const tex_a { strings.intern( "textures/rail.dds" ) };
|
||||
auto const tex_b { strings.intern( "textures/grass.dds" ) };
|
||||
auto const proto_a { strings.intern( "models/sem.e3d" ) };
|
||||
(void)tex_b;
|
||||
(void)proto_a;
|
||||
|
||||
std::vector<sample_instance> const instances {
|
||||
{ 0, 10.0, 0.0, -5.0, 1.5707963f, tex_a },
|
||||
{ 1, 1234.5, 12.25, -6789.0, 0.0f, 0xffffffffu },
|
||||
{ 2, -42.0, 3.0, 7.0, 3.14159f, tex_b },
|
||||
};
|
||||
|
||||
byte_writer strs_payload;
|
||||
strings.serialize( strs_payload );
|
||||
|
||||
byte_writer inst_payload;
|
||||
inst_payload.put_u32( static_cast<std::uint32_t>( instances.size() ) );
|
||||
for( auto const &i : instances ) {
|
||||
inst_payload.put_u32( i.proto_id );
|
||||
inst_payload.put_f64( i.x );
|
||||
inst_payload.put_f64( i.y );
|
||||
inst_payload.put_f64( i.z );
|
||||
inst_payload.put_f32( i.yaw );
|
||||
inst_payload.put_u32( i.texture_override );
|
||||
}
|
||||
|
||||
container_writer writer( file_kind::tile );
|
||||
writer.add_chunk( chunk::strs, strs_payload );
|
||||
writer.add_chunk( chunk::inst, inst_payload );
|
||||
return writer.data();
|
||||
}
|
||||
|
||||
void
|
||||
verify_tile( std::vector<std::uint8_t> const &bytes ) {
|
||||
container_reader reader( bytes.data(), bytes.size() );
|
||||
check( reader.kind() == file_kind::tile, "tile kind" );
|
||||
check( reader.version() == kVersion, "tile version" );
|
||||
|
||||
string_pool pool;
|
||||
bool saw_strs { false };
|
||||
bool saw_inst { false };
|
||||
|
||||
chunk_view chunk;
|
||||
while( reader.next( chunk ) ) {
|
||||
if( chunk.id == chunk::strs ) {
|
||||
saw_strs = true;
|
||||
auto r { chunk.reader() };
|
||||
pool.deserialize( r );
|
||||
check( pool.size() == 3, "string count" );
|
||||
check( pool.get( 0 ) == "textures/rail.dds", "string 0" );
|
||||
check( pool.get( 2 ) == "models/sem.e3d", "string 2" );
|
||||
}
|
||||
else if( chunk.id == chunk::inst ) {
|
||||
saw_inst = true;
|
||||
auto r { chunk.reader() };
|
||||
auto const count { r.get_u32() };
|
||||
check( count == 3, "instance count" );
|
||||
|
||||
auto const p0 { r.get_u32() };
|
||||
auto const x0 { r.get_f64() };
|
||||
auto const y0 { r.get_f64() };
|
||||
auto const z0 { r.get_f64() };
|
||||
auto const yaw0 { r.get_f32() };
|
||||
auto const tex0 { r.get_u32() };
|
||||
check( p0 == 0, "inst0 proto" );
|
||||
check( x0 == 10.0 && y0 == 0.0 && z0 == -5.0, "inst0 pos (f64 exact)" );
|
||||
check( std::fabs( yaw0 - 1.5707963f ) < 1e-6f, "inst0 yaw" );
|
||||
check( tex0 == 0, "inst0 tex override id" );
|
||||
|
||||
// second record: verify large f64 coordinate keeps full precision
|
||||
auto const p1 { r.get_u32() };
|
||||
auto const x1 { r.get_f64() };
|
||||
(void)r.get_f64();
|
||||
auto const z1 { r.get_f64() };
|
||||
(void)r.get_f32();
|
||||
auto const tex1 { r.get_u32() };
|
||||
check( p1 == 1, "inst1 proto" );
|
||||
check( x1 == 1234.5 && z1 == -6789.0, "inst1 large coord precision" );
|
||||
check( tex1 == 0xffffffffu, "inst1 no tex override" );
|
||||
}
|
||||
}
|
||||
|
||||
check( saw_strs, "tile has STRS" );
|
||||
check( saw_inst, "tile has INST" );
|
||||
}
|
||||
|
||||
void
|
||||
verify_trgr_roundtrip() {
|
||||
std::vector<sample_edge> const edges {
|
||||
{ 0, 1, 1, 0 },
|
||||
{ 1, 2, 1, 0 },
|
||||
{ 2, 0, 1, 0 },
|
||||
};
|
||||
|
||||
byte_writer payload;
|
||||
payload.put_u32( static_cast<std::uint32_t>( edges.size() ) );
|
||||
for( auto const &e : edges ) {
|
||||
payload.put_u32( e.track );
|
||||
payload.put_u32( e.neighbour );
|
||||
payload.put_u8( e.end );
|
||||
payload.put_u8( e.neighbour_end );
|
||||
}
|
||||
|
||||
container_writer writer( file_kind::sim );
|
||||
writer.add_chunk( chunk::trgr, payload );
|
||||
auto const bytes { writer.data() };
|
||||
|
||||
container_reader reader( bytes.data(), bytes.size() );
|
||||
check( reader.kind() == file_kind::sim, "sim kind" );
|
||||
|
||||
chunk_view chunk;
|
||||
bool ok { false };
|
||||
while( reader.next( chunk ) ) {
|
||||
if( chunk.id != chunk::trgr ) {
|
||||
continue;
|
||||
}
|
||||
auto r { chunk.reader() };
|
||||
auto const count { r.get_u32() };
|
||||
check( count == edges.size(), "edge count" );
|
||||
for( auto const &expected : edges ) {
|
||||
auto const track { r.get_u32() };
|
||||
auto const neighbour { r.get_u32() };
|
||||
auto const end { r.get_u8() };
|
||||
auto const nend { r.get_u8() };
|
||||
check(
|
||||
track == expected.track && neighbour == expected.neighbour &&
|
||||
end == expected.end && nend == expected.neighbour_end,
|
||||
"edge values" );
|
||||
}
|
||||
ok = true;
|
||||
}
|
||||
check( ok, "sim has TRGR" );
|
||||
}
|
||||
|
||||
void
|
||||
verify_truncation_is_rejected() {
|
||||
auto bytes { build_tile() };
|
||||
bytes.resize( bytes.size() - 4 ); // chop the tail of the last chunk
|
||||
bool threw { false };
|
||||
try {
|
||||
container_reader reader( bytes.data(), bytes.size() );
|
||||
chunk_view chunk;
|
||||
while( reader.next( chunk ) ) {
|
||||
auto r { chunk.reader() };
|
||||
// force payload reads
|
||||
while( r.remaining() > 0 ) {
|
||||
(void)r.get_u8();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( parse_error const & ) {
|
||||
threw = true;
|
||||
}
|
||||
check( threw, "truncated file rejected with parse_error" );
|
||||
}
|
||||
|
||||
void
|
||||
verify_scene_roundtrip() {
|
||||
string_table strings;
|
||||
auto const sem_model { strings.intern( "models/sem.e3d" ) };
|
||||
auto const sem_tex { strings.intern( "textures/sem.dds" ) };
|
||||
auto const grass { strings.intern( "textures/grass.dds" ) };
|
||||
auto const tex_override { strings.intern( "textures/sem_winter.dds" ) };
|
||||
|
||||
std::vector<model_prototype> protos( 2 );
|
||||
protos[ 0 ].model_file = sem_model;
|
||||
protos[ 0 ].texture_file = sem_tex;
|
||||
protos[ 0 ].flags = proto_flag::instanceable | proto_flag::transition;
|
||||
protos[ 0 ].range_min = 0.f;
|
||||
protos[ 0 ].range_max = 4000.f;
|
||||
protos[ 0 ].light_states = { 1.f, 0.5f, 0.f };
|
||||
protos[ 0 ].light_colors = { 0xff0000ffu, 0x00ff00ffu };
|
||||
protos[ 1 ].model_file = strings.intern( "models/tree.e3d" );
|
||||
protos[ 1 ].flags = proto_flag::instanceable;
|
||||
|
||||
std::vector<model_instance> instances( 3 );
|
||||
instances[ 0 ] = { 0, 100.0, 0.0, -200.0, 0.f, 90.f, 0.f, 1.f, 1.f, 1.f, kNoString, 7 };
|
||||
instances[ 1 ] = { 0, 999999.5, 1.25, -888888.0, 0.f, 0.f, 0.f, 2.f, 2.f, 2.f, tex_override, 3 };
|
||||
instances[ 2 ] = { 1, -5.0, 0.0, 5.0, 0.f, 45.f, 0.f, 1.f, 1.f, 1.f, kNoString, 0xffu };
|
||||
|
||||
std::vector<terrain_mesh> meshes( 1 );
|
||||
meshes[ 0 ].material = grass;
|
||||
meshes[ 0 ].translucent = false;
|
||||
meshes[ 0 ].ox = 500000.0;
|
||||
meshes[ 0 ].oy = 0.0;
|
||||
meshes[ 0 ].oz = -500000.0;
|
||||
meshes[ 0 ].vertices = {
|
||||
{ 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f },
|
||||
{ 1000.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f },
|
||||
{ 0.f, 0.f, -1000.f, 0.f, 1.f, 0.f, 0.f, 1.f },
|
||||
};
|
||||
|
||||
byte_writer strs_payload;
|
||||
strings.serialize( strs_payload );
|
||||
byte_writer prot_payload;
|
||||
write_prototypes( prot_payload, protos );
|
||||
byte_writer inst_payload;
|
||||
write_instances( inst_payload, instances );
|
||||
byte_writer mesh_payload;
|
||||
write_terrain_meshes( mesh_payload, meshes );
|
||||
|
||||
container_writer writer( file_kind::tile );
|
||||
writer.add_chunk( chunk::strs, strs_payload );
|
||||
writer.add_chunk( chunk::prot, prot_payload );
|
||||
writer.add_chunk( chunk::inst, inst_payload );
|
||||
writer.add_chunk( chunk::mesh, mesh_payload );
|
||||
auto const bytes { writer.data() };
|
||||
|
||||
container_reader reader( bytes.data(), bytes.size() );
|
||||
string_pool pool;
|
||||
std::vector<model_prototype> rp;
|
||||
std::vector<model_instance> ri;
|
||||
std::vector<terrain_mesh> rm;
|
||||
chunk_view chunk;
|
||||
while( reader.next( chunk ) ) {
|
||||
auto r { chunk.reader() };
|
||||
if( chunk.id == chunk::strs ) {
|
||||
pool.deserialize( r );
|
||||
}
|
||||
else if( chunk.id == chunk::prot ) {
|
||||
rp = read_prototypes( r );
|
||||
}
|
||||
else if( chunk.id == chunk::inst ) {
|
||||
ri = read_instances( r );
|
||||
}
|
||||
else if( chunk.id == chunk::mesh ) {
|
||||
rm = read_terrain_meshes( r );
|
||||
}
|
||||
}
|
||||
|
||||
check( rp.size() == 2, "proto count" );
|
||||
check( rp[ 0 ].flags == ( proto_flag::instanceable | proto_flag::transition ), "proto0 flags" );
|
||||
check( rp[ 0 ].range_max == 4000.f, "proto0 range" );
|
||||
check( rp[ 0 ].light_states.size() == 3 && rp[ 0 ].light_states[ 1 ] == 0.5f, "proto0 light states" );
|
||||
check( rp[ 0 ].light_colors.size() == 2 && rp[ 0 ].light_colors[ 0 ] == 0xff0000ffu, "proto0 light colors" );
|
||||
check( pool.get( rp[ 0 ].model_file ) == "models/sem.e3d", "proto0 model string" );
|
||||
|
||||
check( ri.size() == 3, "instance count" );
|
||||
check( ri[ 0 ].proto == 0 && ri[ 0 ].cell_id == 7, "inst0 proto/cell" );
|
||||
check( ri[ 0 ].sx == 1.f && ri[ 0 ].texture_override == kNoString, "inst0 defaults survive" );
|
||||
check( ri[ 1 ].x == 999999.5 && ri[ 1 ].z == -888888.0, "inst1 large coord precision" );
|
||||
check( ri[ 1 ].sx == 2.f && ri[ 1 ].sy == 2.f && ri[ 1 ].sz == 2.f, "inst1 scale" );
|
||||
check( pool.get( ri[ 1 ].texture_override ) == "textures/sem_winter.dds", "inst1 override" );
|
||||
check( ri[ 2 ].cell_id == 0xffu, "inst2 no cell" );
|
||||
|
||||
check( rm.size() == 1, "mesh count" );
|
||||
check( rm[ 0 ].ox == 500000.0 && rm[ 0 ].oz == -500000.0, "mesh origin f64 precision" );
|
||||
check( rm[ 0 ].vertices.size() == 3, "mesh vertex count" );
|
||||
check( rm[ 0 ].vertices[ 1 ].px == 1000.f && rm[ 0 ].vertices[ 1 ].u == 1.f, "mesh vertex data" );
|
||||
check( pool.get( rm[ 0 ].material ) == "textures/grass.dds", "mesh material string" );
|
||||
}
|
||||
|
||||
void
|
||||
verify_records_roundtrip() {
|
||||
string_table s;
|
||||
|
||||
// --- tracks ---
|
||||
std::vector<track_record> tracks( 1 );
|
||||
tracks[ 0 ].node.name = s.intern( "track_42" );
|
||||
tracks[ 0 ].node.type = s.intern( "track" );
|
||||
tracks[ 0 ].node.area_center = { 123456.5, 0.0, -654321.0 };
|
||||
tracks[ 0 ].node.range_sq_max = 1.0e12;
|
||||
tracks[ 0 ].track_type = 2; // switch
|
||||
tracks[ 0 ].category = 1;
|
||||
tracks[ 0 ].length = 50.0f;
|
||||
tracks[ 0 ].environment = 3;
|
||||
tracks[ 0 ].has_visibility = true;
|
||||
tracks[ 0 ].visibility.material1 = s.intern( "rail.mat" );
|
||||
tracks[ 0 ].visibility.tex_length = 8.f;
|
||||
tracks[ 0 ].paths.push_back( { { 1.0, 2.0, 3.0 }, 0.1, { 4.0, 5.0, 6.0 }, { 7.0, 8.0, 9.0 }, { 10.0, 11.0, 12.0 }, 0.2, 300.0 } );
|
||||
tracks[ 0 ].tail_keywords.emplace_back( s.intern( "event0" ), s.intern( "ev_start" ) );
|
||||
|
||||
// --- traction ---
|
||||
std::vector<traction_record> traction( 1 );
|
||||
traction[ 0 ].node.name = s.intern( "trakcja1" );
|
||||
traction[ 0 ].power_supply_name = s.intern( "psupply" );
|
||||
traction[ 0 ].nominal_voltage = 3000.f;
|
||||
traction[ 0 ].material = 1;
|
||||
traction[ 0 ].wire_p1 = { 0.0, 6.0, 0.0 };
|
||||
traction[ 0 ].wire_p2 = { 100.0, 6.0, 0.0 };
|
||||
traction[ 0 ].wire_count = 2;
|
||||
traction[ 0 ].has_parallel = true;
|
||||
traction[ 0 ].parallel_name = s.intern( "trakcja0" );
|
||||
|
||||
// --- power sources ---
|
||||
std::vector<power_source_record> power( 1 );
|
||||
power[ 0 ].node.name = s.intern( "psupply" );
|
||||
power[ 0 ].position = { 50.0, 0.0, -50.0 };
|
||||
power[ 0 ].nominal_voltage = 3000.f;
|
||||
power[ 0 ].max_output_current = 4000.f;
|
||||
power[ 0 ].modifier = 2;
|
||||
|
||||
// --- memcells ---
|
||||
std::vector<memcell_record> memcells( 1 );
|
||||
memcells[ 0 ].node.name = s.intern( "mc_station" );
|
||||
memcells[ 0 ].text = s.intern( "PASS depot 1" );
|
||||
memcells[ 0 ].value1 = 12.5;
|
||||
memcells[ 0 ].value2 = -7.0;
|
||||
memcells[ 0 ].has_track = true;
|
||||
memcells[ 0 ].track_name = s.intern( "track_42" );
|
||||
|
||||
// --- launchers ---
|
||||
std::vector<launcher_record> launchers( 1 );
|
||||
launchers[ 0 ].node.name = s.intern( "launch1" );
|
||||
launchers[ 0 ].location = { 200.0, 1.0, -200.0 };
|
||||
launchers[ 0 ].radius_squared = 400.0;
|
||||
launchers[ 0 ].activation_key = 65;
|
||||
launchers[ 0 ].event1_name = s.intern( "ev_start" );
|
||||
launchers[ 0 ].has_condition = true;
|
||||
launchers[ 0 ].condition.memcell_name = s.intern( "mc_station" );
|
||||
launchers[ 0 ].condition.compare_value1 = 12.5;
|
||||
launchers[ 0 ].condition.check_mask = 3;
|
||||
launchers[ 0 ].launch_hour = 14;
|
||||
launchers[ 0 ].launch_minute = 30;
|
||||
|
||||
// --- events ---
|
||||
std::vector<event_record> events( 1 );
|
||||
events[ 0 ].name = s.intern( "ev_start" );
|
||||
events[ 0 ].type = 7; // multiple
|
||||
events[ 0 ].delay = 2.5;
|
||||
events[ 0 ].passive = true;
|
||||
events[ 0 ].targets = { s.intern( "ev_a" ), s.intern( "ev_b" ) };
|
||||
events[ 0 ].payload.emplace_back( s.intern( "k1" ), s.intern( "v1" ) );
|
||||
|
||||
// --- sounds ---
|
||||
std::vector<sound_record> sounds( 1 );
|
||||
sounds[ 0 ].node.name = s.intern( "snd1" );
|
||||
sounds[ 0 ].location = { -10.0, 2.0, 10.0 };
|
||||
sounds[ 0 ].wav_file = s.intern( "ambient.wav" );
|
||||
|
||||
// --- dynamics ---
|
||||
std::vector<dynamic_record> dynamics( 1 );
|
||||
dynamics[ 0 ].node.name = s.intern( "veh1" );
|
||||
dynamics[ 0 ].data_folder = s.intern( "pkp/ep07" );
|
||||
dynamics[ 0 ].mmd_file = s.intern( "ep07.mmd" );
|
||||
dynamics[ 0 ].track_name = s.intern( "track_42" );
|
||||
dynamics[ 0 ].offset = 25.0;
|
||||
dynamics[ 0 ].coupling = 3;
|
||||
dynamics[ 0 ].velocity = 40.f;
|
||||
dynamics[ 0 ].has_destination = true;
|
||||
dynamics[ 0 ].destination = s.intern( "Warszawa" );
|
||||
dynamics[ 0 ].has_trainset = true;
|
||||
dynamics[ 0 ].trainset_index = 5;
|
||||
|
||||
byte_writer strs_payload, trak_p, trac_p, pwrs_p, memc_p, laun_p, evnt_p, sond_p, dynm_p;
|
||||
s.serialize( strs_payload );
|
||||
write_tracks( trak_p, tracks );
|
||||
write_traction( trac_p, traction );
|
||||
write_power_sources( pwrs_p, power );
|
||||
write_memcells( memc_p, memcells );
|
||||
write_launchers( laun_p, launchers );
|
||||
write_events( evnt_p, events );
|
||||
write_sounds( sond_p, sounds );
|
||||
write_dynamics( dynm_p, dynamics );
|
||||
|
||||
container_writer w( file_kind::sim );
|
||||
w.add_chunk( chunk::strs, strs_payload );
|
||||
w.add_chunk( chunk::trak, trak_p );
|
||||
w.add_chunk( chunk::trac, trac_p );
|
||||
w.add_chunk( chunk::pwrs, pwrs_p );
|
||||
w.add_chunk( chunk::memc, memc_p );
|
||||
w.add_chunk( chunk::laun, laun_p );
|
||||
w.add_chunk( chunk::evnt, evnt_p );
|
||||
w.add_chunk( chunk::sond, sond_p );
|
||||
w.add_chunk( chunk::dynm, dynm_p );
|
||||
auto const bytes { w.data() };
|
||||
|
||||
container_reader r( bytes.data(), bytes.size() );
|
||||
check( r.kind() == file_kind::sim, "records sim kind" );
|
||||
|
||||
string_pool pool;
|
||||
std::vector<track_record> rt;
|
||||
std::vector<traction_record> rtr;
|
||||
std::vector<power_source_record> rp;
|
||||
std::vector<memcell_record> rmc;
|
||||
std::vector<launcher_record> rl;
|
||||
std::vector<event_record> re;
|
||||
std::vector<sound_record> rs;
|
||||
std::vector<dynamic_record> rd;
|
||||
chunk_view c;
|
||||
while( r.next( c ) ) {
|
||||
auto rr { c.reader() };
|
||||
switch( c.id ) {
|
||||
case chunk::strs: pool.deserialize( rr ); break;
|
||||
case chunk::trak: rt = read_tracks( rr ); break;
|
||||
case chunk::trac: rtr = read_traction( rr ); break;
|
||||
case chunk::pwrs: rp = read_power_sources( rr ); break;
|
||||
case chunk::memc: rmc = read_memcells( rr ); break;
|
||||
case chunk::laun: rl = read_launchers( rr ); break;
|
||||
case chunk::evnt: re = read_events( rr ); break;
|
||||
case chunk::sond: rs = read_sounds( rr ); break;
|
||||
case chunk::dynm: rd = read_dynamics( rr ); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
check( rt.size() == 1 && pool.get( rt[ 0 ].node.name ) == "track_42", "track name" );
|
||||
check( rt[ 0 ].node.area_center.x == 123456.5 && rt[ 0 ].node.area_center.z == -654321.0, "track center f64" );
|
||||
check( rt[ 0 ].track_type == 2 && rt[ 0 ].environment == 3, "track type/env" );
|
||||
check( rt[ 0 ].has_visibility && pool.get( rt[ 0 ].visibility.material1 ) == "rail.mat", "track visibility" );
|
||||
check( rt[ 0 ].paths.size() == 1 && rt[ 0 ].paths[ 0 ].radius == 300.0, "track path" );
|
||||
check( rt[ 0 ].paths[ 0 ].p_end.z == 12.0, "track path endpoint" );
|
||||
check( rt[ 0 ].tail_keywords.size() == 1 && pool.get( rt[ 0 ].tail_keywords[ 0 ].second ) == "ev_start", "track keyword" );
|
||||
|
||||
check( rtr.size() == 1 && rtr[ 0 ].nominal_voltage == 3000.f, "traction voltage" );
|
||||
check( rtr[ 0 ].wire_p2.x == 100.0 && rtr[ 0 ].wire_count == 2, "traction wire" );
|
||||
check( rtr[ 0 ].has_parallel && pool.get( rtr[ 0 ].parallel_name ) == "trakcja0", "traction parallel" );
|
||||
|
||||
check( rp.size() == 1 && rp[ 0 ].max_output_current == 4000.f && rp[ 0 ].modifier == 2, "power source" );
|
||||
|
||||
check( rmc.size() == 1 && pool.get( rmc[ 0 ].text ) == "PASS depot 1", "memcell text" );
|
||||
check( rmc[ 0 ].value1 == 12.5 && rmc[ 0 ].has_track, "memcell value/track" );
|
||||
|
||||
check( rl.size() == 1 && rl[ 0 ].activation_key == 65, "launcher key" );
|
||||
check( rl[ 0 ].has_condition && rl[ 0 ].condition.check_mask == 3, "launcher condition" );
|
||||
check( rl[ 0 ].launch_hour == 14 && rl[ 0 ].launch_minute == 30, "launcher time" );
|
||||
|
||||
check( re.size() == 1 && re[ 0 ].type == 7 && re[ 0 ].passive, "event type" );
|
||||
check( re[ 0 ].targets.size() == 2 && pool.get( re[ 0 ].targets[ 1 ] ) == "ev_b", "event targets" );
|
||||
check( re[ 0 ].payload.size() == 1 && pool.get( re[ 0 ].payload[ 0 ].first ) == "k1", "event payload" );
|
||||
|
||||
check( rs.size() == 1 && pool.get( rs[ 0 ].wav_file ) == "ambient.wav", "sound wav" );
|
||||
|
||||
check( rd.size() == 1 && pool.get( rd[ 0 ].data_folder ) == "pkp/ep07", "dynamic folder" );
|
||||
check( rd[ 0 ].offset == 25.0 && rd[ 0 ].velocity == 40.f, "dynamic offset/vel" );
|
||||
check( rd[ 0 ].has_destination && pool.get( rd[ 0 ].destination ) == "Warszawa", "dynamic destination" );
|
||||
check( rd[ 0 ].has_trainset && rd[ 0 ].trainset_index == 5, "dynamic trainset" );
|
||||
}
|
||||
|
||||
void
|
||||
verify_extensions_roundtrip() {
|
||||
string_table s;
|
||||
|
||||
// --- shapes (SHPE) ---
|
||||
std::vector<shape_record> shapes( 1 );
|
||||
shapes[ 0 ].node.name = s.intern( "shape0" );
|
||||
shapes[ 0 ].node.type = s.intern( "triangles" );
|
||||
shapes[ 0 ].translucent = true;
|
||||
shapes[ 0 ].material = s.intern( "bricks.mat" );
|
||||
shapes[ 0 ].lighting.diffuse[ 0 ] = 0.5f;
|
||||
shapes[ 0 ].ox = 12345.5;
|
||||
shapes[ 0 ].oy = 1.0;
|
||||
shapes[ 0 ].oz = -54321.0;
|
||||
shapes[ 0 ].vertices = {
|
||||
{ 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f },
|
||||
{ 10.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 0.f },
|
||||
{ 0.f, 5.f, 0.f, 0.f, 1.f, 0.f, 0.f, 1.f },
|
||||
};
|
||||
|
||||
// --- lines (LINE) ---
|
||||
std::vector<lines_record> lines( 1 );
|
||||
lines[ 0 ].node.name = s.intern( "line0" );
|
||||
lines[ 0 ].line_width = 2.f;
|
||||
lines[ 0 ].ox = 100.0;
|
||||
lines[ 0 ].vertices = { { 0.0, 0.0, 0.0 }, { 50.0, 0.0, -3.0 } };
|
||||
|
||||
// --- trainsets (TRST) ---
|
||||
std::vector<trainset_record> trainsets( 1 );
|
||||
trainsets[ 0 ].name = s.intern( "ts_warszawa" );
|
||||
trainsets[ 0 ].track = s.intern( "track_42" );
|
||||
trainsets[ 0 ].offset = 25.5f;
|
||||
trainsets[ 0 ].velocity = 60.f;
|
||||
trainsets[ 0 ].assignment.emplace_back( s.intern( "k" ), s.intern( "v" ) );
|
||||
trainsets[ 0 ].vehicle_indices = { 0u, 1u, 2u };
|
||||
trainsets[ 0 ].couplings = { 3, -1 };
|
||||
trainsets[ 0 ].driver_index = 1u;
|
||||
|
||||
// --- includes (INCL) ---
|
||||
std::vector<include_record> includes( 1 );
|
||||
includes[ 0 ].source_line = 17;
|
||||
includes[ 0 ].source_path = s.intern( "scenery/sub.inc" );
|
||||
includes[ 0 ].binary_path = s.intern( "scenery/sub.eu7v2" );
|
||||
includes[ 0 ].parameters = { s.intern( "p1" ), s.intern( "p2" ) };
|
||||
includes[ 0 ].site_transform.origin_stack.push_back( { 10.0, 0.0, -20.0 } );
|
||||
includes[ 0 ].site_transform.rotation = { 0.0, 90.0, 0.0 };
|
||||
includes[ 0 ].site_transform.group_depth = 1;
|
||||
|
||||
// --- instance with full node ---
|
||||
std::vector<model_prototype> protos( 1 );
|
||||
protos[ 0 ].model_file = s.intern( "models/x.e3d" );
|
||||
std::vector<model_instance> instances( 1 );
|
||||
instances[ 0 ].proto = 0;
|
||||
instances[ 0 ].x = 7.0;
|
||||
instances[ 0 ].has_node = true;
|
||||
instances[ 0 ].node.name = s.intern( "inst_named" );
|
||||
instances[ 0 ].node.type = s.intern( "model" );
|
||||
instances[ 0 ].node.range_sq_max = 1.0e9;
|
||||
instances[ 0 ].node.visible = false;
|
||||
|
||||
// --- meta (META) ---
|
||||
module_meta meta;
|
||||
meta.first_init_count = 4;
|
||||
meta.has_terrain_chunk = true;
|
||||
meta.placement_origin_x = 1;
|
||||
meta.placement_rotation_y = 4;
|
||||
|
||||
byte_writer strs_p, shpe_p, line_p, trst_p, incl_p, prot_p, inst_p, meta_p;
|
||||
s.serialize( strs_p );
|
||||
write_shapes( shpe_p, shapes );
|
||||
write_lines( line_p, lines );
|
||||
write_trainsets( trst_p, trainsets );
|
||||
write_includes( incl_p, includes );
|
||||
write_prototypes( prot_p, protos );
|
||||
write_instances( inst_p, instances );
|
||||
write_meta( meta_p, meta );
|
||||
|
||||
container_writer w( file_kind::module );
|
||||
w.add_chunk( chunk::strs, strs_p );
|
||||
w.add_chunk( chunk::meta, meta_p );
|
||||
w.add_chunk( chunk::incl, incl_p );
|
||||
w.add_chunk( chunk::shpe, shpe_p );
|
||||
w.add_chunk( chunk::line, line_p );
|
||||
w.add_chunk( chunk::trst, trst_p );
|
||||
w.add_chunk( chunk::prot, prot_p );
|
||||
w.add_chunk( chunk::inst, inst_p );
|
||||
auto const bytes { w.data() };
|
||||
|
||||
container_reader r( bytes.data(), bytes.size() );
|
||||
check( r.kind() == file_kind::module, "ext module kind" );
|
||||
|
||||
string_pool pool;
|
||||
std::vector<shape_record> rsh;
|
||||
std::vector<lines_record> rln;
|
||||
std::vector<trainset_record> rts;
|
||||
std::vector<include_record> rin;
|
||||
std::vector<model_instance> ri;
|
||||
module_meta rmeta;
|
||||
chunk_view c;
|
||||
while( r.next( c ) ) {
|
||||
auto rr { c.reader() };
|
||||
switch( c.id ) {
|
||||
case chunk::strs: pool.deserialize( rr ); break;
|
||||
case chunk::meta: rmeta = read_meta( rr ); break;
|
||||
case chunk::incl: rin = read_includes( rr ); break;
|
||||
case chunk::shpe: rsh = read_shapes( rr ); break;
|
||||
case chunk::line: rln = read_lines( rr ); break;
|
||||
case chunk::trst: rts = read_trainsets( rr ); break;
|
||||
case chunk::inst: ri = read_instances( rr ); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
check( rsh.size() == 1 && rsh[ 0 ].translucent, "shape translucent" );
|
||||
check( rsh[ 0 ].ox == 12345.5 && rsh[ 0 ].oz == -54321.0, "shape origin f64" );
|
||||
check( rsh[ 0 ].vertices.size() == 3 && rsh[ 0 ].vertices[ 1 ].px == 10.f, "shape verts" );
|
||||
check( pool.get( rsh[ 0 ].material ) == "bricks.mat", "shape material" );
|
||||
check( rsh[ 0 ].lighting.diffuse[ 0 ] == 0.5f, "shape lighting" );
|
||||
|
||||
check( rln.size() == 1 && rln[ 0 ].line_width == 2.f, "line width" );
|
||||
check( rln[ 0 ].vertices.size() == 2 && rln[ 0 ].vertices[ 1 ].x == 50.0, "line verts" );
|
||||
|
||||
check( rts.size() == 1 && pool.get( rts[ 0 ].name ) == "ts_warszawa", "trainset name" );
|
||||
check( rts[ 0 ].vehicle_indices.size() == 3 && rts[ 0 ].vehicle_indices[ 2 ] == 2u, "trainset vehicles" );
|
||||
check( rts[ 0 ].couplings.size() == 2 && rts[ 0 ].couplings[ 1 ] == -1, "trainset couplings" );
|
||||
check( rts[ 0 ].driver_index == 1u && rts[ 0 ].offset == 25.5f, "trainset driver/offset" );
|
||||
|
||||
check( rin.size() == 1 && rin[ 0 ].source_line == 17, "include line" );
|
||||
check( pool.get( rin[ 0 ].binary_path ) == "scenery/sub.eu7v2", "include binary path .eu7v2" );
|
||||
check( rin[ 0 ].parameters.size() == 2, "include params" );
|
||||
check( rin[ 0 ].site_transform.origin_stack.size() == 1 &&
|
||||
rin[ 0 ].site_transform.origin_stack[ 0 ].z == -20.0, "include transform origin" );
|
||||
check( rin[ 0 ].site_transform.rotation.y == 90.0 &&
|
||||
rin[ 0 ].site_transform.group_depth == 1, "include transform rot/group" );
|
||||
|
||||
check( ri.size() == 1 && ri[ 0 ].has_node, "inst has node" );
|
||||
check( pool.get( ri[ 0 ].node.name ) == "inst_named" && !ri[ 0 ].node.visible, "inst node fields" );
|
||||
check( ri[ 0 ].node.range_sq_max == 1.0e9, "inst node range" );
|
||||
|
||||
check( rmeta.first_init_count == 4 && rmeta.has_terrain_chunk, "meta counts/flags" );
|
||||
check( rmeta.placement_origin_x == 1 && rmeta.placement_rotation_y == 4, "meta placement" );
|
||||
}
|
||||
|
||||
void
|
||||
verify_file_io() {
|
||||
auto const bytes { build_tile() };
|
||||
char const *path { "eu7v2_roundtrip.bin" };
|
||||
{
|
||||
std::ofstream os( path, std::ios::binary );
|
||||
os.write( reinterpret_cast<char const *>( bytes.data() ),
|
||||
static_cast<std::streamsize>( bytes.size() ) );
|
||||
}
|
||||
std::ifstream is( path, std::ios::binary | std::ios::ate );
|
||||
auto const size { static_cast<std::size_t>( is.tellg() ) };
|
||||
is.seekg( 0 );
|
||||
std::vector<std::uint8_t> loaded( size );
|
||||
is.read( reinterpret_cast<char *>( loaded.data() ),
|
||||
static_cast<std::streamsize>( size ) );
|
||||
check( loaded == bytes, "file write/read byte-identical" );
|
||||
verify_tile( loaded );
|
||||
std::remove( path );
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int
|
||||
main() {
|
||||
std::printf( "eu7v2 format core round-trip test\n" );
|
||||
|
||||
auto const tile { build_tile() };
|
||||
verify_tile( tile );
|
||||
verify_trgr_roundtrip();
|
||||
verify_scene_roundtrip();
|
||||
verify_records_roundtrip();
|
||||
verify_extensions_roundtrip();
|
||||
verify_truncation_is_rejected();
|
||||
verify_file_io();
|
||||
|
||||
std::printf( "checks: %d, failures: %d\n", g_checks, g_failures );
|
||||
if( g_failures == 0 ) {
|
||||
std::printf( "OK\n" );
|
||||
return 0;
|
||||
}
|
||||
std::printf( "FAILED\n" );
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user