mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-07-17 23:39:18 +02:00
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>
674 lines
26 KiB
C++
674 lines
26 KiB
C++
/*
|
|
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;
|
|
}
|