diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f34ad61..8a4e1d6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,7 @@ set(SOURCES "world/station.cpp" "application/application.cpp" "simulation/simulationtime.cpp" +"simulation/simulationtest.cpp" "scene/sceneeditor.cpp" "rendering/screenshot.cpp" "input/driverkeyboardinput.cpp" diff --git a/EU07.cpp b/EU07.cpp index e0361829..aac7cae1 100644 --- a/EU07.cpp +++ b/EU07.cpp @@ -19,6 +19,8 @@ Stele, firleju, szociu, hunter, ZiomalCl, OLI_EU and others #include "stdafx.h" #include "application/application.h" +#include "simulation/simulationtest.h" +#include "utilities/Globals.h" #include "utilities/Logs.h" #include #ifdef WITHDUMPGEN @@ -148,9 +150,14 @@ int main(int argc, char *argv[]) #ifdef _WIN32 catch (std::runtime_error const &Error) { - std::string msg = "Simulator crash occured :(\n"; - msg += Error.what(); - MessageBoxA(nullptr, msg.c_str(), "Simulator crashed :(", MB_ICONERROR); + ErrorLog("Critical error: " + std::string(Error.what())); + // in test mode nobody is there to close the box, the exit code tells of the error + if (false == Global.test.enabled) + { + std::string msg = "Simulator crash occured :(\n"; + msg += Error.what(); + MessageBoxA(nullptr, msg.c_str(), "Simulator crashed :(", MB_ICONERROR); + } } #endif } @@ -158,5 +165,7 @@ int main(int argc, char *argv[]) fflush(stdout); fflush(stderr); #endif - std::_Exit(0); // skip destructors, there are ordering errors which causes segfaults + // skip destructors, there are ordering errors which causes segfaults + // a test run (--test) says its result in the exit code + std::_Exit(Global.test.enabled ? simulation::Test.exit_code() : 0); } diff --git a/application/application.cpp b/application/application.cpp index ee381f98..37afff67 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -378,7 +378,7 @@ int eu07_application::init(int Argc, char *Argv[]) return result; } - if (crashreport_is_pending()) + if (crashreport_is_pending() && false == Global.test.enabled) { // run crashgui as early as possible if ((result = run_crashgui()) != 0) { @@ -951,14 +951,17 @@ GLFWwindow *eu07_application::window(int const Windowindex, bool visible, int wi } // for index -1 create a new child window - auto const *vmode{glfwGetVideoMode(monitor ? monitor : glfwGetPrimaryMonitor())}; - - // match requested video mode to current to allow for - // fullwindow creation when resolution is the same - glfwWindowHint(GLFW_RED_BITS, vmode->redBits); - glfwWindowHint(GLFW_GREEN_BITS, vmode->greenBits); - glfwWindowHint(GLFW_BLUE_BITS, vmode->blueBits); - glfwWindowHint(GLFW_REFRESH_RATE, vmode->refreshRate); + auto *videomonitor{monitor ? monitor : glfwGetPrimaryMonitor()}; + // a machine without a display (e.g. a test runner) has no monitor to match + if (auto const *vmode{videomonitor ? glfwGetVideoMode(videomonitor) : nullptr}) + { + // match requested video mode to current to allow for + // fullwindow creation when resolution is the same + glfwWindowHint(GLFW_RED_BITS, vmode->redBits); + glfwWindowHint(GLFW_GREEN_BITS, vmode->greenBits); + glfwWindowHint(GLFW_BLUE_BITS, vmode->blueBits); + glfwWindowHint(GLFW_REFRESH_RATE, vmode->refreshRate); + } glfwWindowHint(GLFW_VISIBLE, visible); @@ -1195,6 +1198,19 @@ int eu07_application::init_settings(int Argc, char *Argv[]) WriteLog(std::format("seed given on the command line: {} -> {}", Argv[i], Global.random_seed)); } } + else if (token == "--test") + { + // integration test: the scenery runs unattended and the exit code is the result (eu07/maszyna#541) + Global.test.enabled = true; + } + else if (token == "--test-timeout") + { + if (i + 1 < Argc) + { + Global.test.timeout = std::max(0.0, std::atof(Argv[++i])); + Global.test.timeout_from_command_line = true; + } + } else if (token == "-ini") { // extra settings file read on top of eu07.ini, so a run can override a few keys @@ -1210,13 +1226,45 @@ int eu07_application::init_settings(int Argc, char *Argv[]) << " [-v vehiclename]" << " [-edit [sceneryfilepath]]" << " [-seed number|word]" - << " [-ini settingsfile]" << std::endl; + << " [-ini settingsfile]" + << " [--test [--test-timeout seconds]]" << std::endl; return -1; } } if (editwithoutscenery && false == sceneryfromcommandline) Global.SceneryFile.clear(); + if (Global.test.enabled) + { + if (false == sceneryfromcommandline || Global.SceneryFile.empty()) + { + ErrorLog("test: --test needs a scenery given with -s"); + return -1; + } + // nobody watches nor listens: no player vehicle, nothing drawn, no sound, no window in the way + Global.editor_session = false; + Global.local_start_vehicle = "ghostview"; + Global.GfxRenderer = "null"; + Global.NvRenderer = false; + Global.bSoundEnabled = false; + Global.python_enabled = false; + Global.bInactivePause = false; + Global.bFullScreen = false; + Global.fullscreen_windowed = false; + Global.VSync = false; + Global.iMultisampling = 0; + Global.gfx_usegles = false; + Global.minframetime = std::chrono::duration(0.0f); + Global.extra_viewports.clear(); + Global.headtrack_conf.joy.clear(); + // a test runs on its own + Global.network_servers.clear(); + Global.network_client.reset(); + // and the same way every time, unless given another seed (-seed, or randomseed in the settings) + if (!Global.random_seed) + Global.random_seed = seed_of("test"); + } + return 0; } @@ -1290,6 +1338,12 @@ int eu07_application::init_glfw() { glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); } + else if (Global.test.enabled) + { + // the null renderer draws nothing, but parts of the ui make textures with gl calls of their own. + // any context will do for them, so none is asked for in particular: a machine without a graphics + // driver (a test runner) has only the basic one of the system + } else { if (!Global.LegacyRenderer) @@ -1341,7 +1395,8 @@ int eu07_application::init_glfw() Global.bFullScreen = true; } - auto *mainwindow = window(-1, true, Global.window_size.x, Global.window_size.y, Global.bFullScreen ? monitor : nullptr, true, false); + // test mode keeps its window hidden: glfw, the ui and the input still need one, but nothing is shown in it + auto *mainwindow = window(-1, false == Global.test.enabled, Global.window_size.x, Global.window_size.y, Global.bFullScreen ? monitor : nullptr, true, false); if (mainwindow == nullptr) { @@ -1428,6 +1483,11 @@ int eu07_application::init_gfx() // default render path GfxRenderer = gfx_renderer_factory::get_instance()->create("modern"); } + else if (Global.GfxRenderer == "null") + { + // draws nothing and loads no textures (test mode) + GfxRenderer = gfx_renderer_factory::get_instance()->create("null"); + } else if (Global.GfxRenderer == "experimental") { GfxRenderer = gfx_renderer_factory::get_instance()->create(Global.GfxRenderer); diff --git a/application/drivermode.cpp b/application/drivermode.cpp index b55d09c2..1b3a44d6 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "simulation/simulationtime.h" #include "simulation/simulationenvironment.h" +#include "simulation/simulationtest.h" #include "scene/scene.h" #include "rendering/lightarray.h" #include "rendering/particles.h" @@ -305,6 +306,11 @@ bool driver_mode::update() simulation::Events.update(); simulation::Region->update_events(); simulation::Lights.update(); + + if (Global.test.enabled && simulation::is_ready) + { + simulation::Test.update(deltatime); + } } // render time routines follow: @@ -478,6 +484,13 @@ void driver_mode::enter() Timer::ResetTimers(); + if (Global.test.enabled) + { + // the test runs as fast as it can compute, in steps of fixed length, so that runs on different machines agree + Timer::set_fixed_step(simulation::test_manager::frame_step); + simulation::Test.start(); + } + set_picking(!Global.captureonstart); m_editorpending = Global.editor_session; diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index f8f06ac9..b84c1912 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "simulation/simulationtime.h" #include "simulation/simulationenvironment.h" +#include "simulation/simulationtest.h" #include "application/application.h" #include "application/scenarioloaderuilayer.h" #include "rendering/renderer.h" @@ -58,6 +59,10 @@ bool scenarioloader_mode::update() { catch (std::runtime_error const &e) { ErrorLog(std::format("Bad init: scenario \"{}\" couldn't be opened ({})", Global.SceneryFile, e.what())); state.reset(); + if (Global.test.enabled) { + simulation::Test.finish(simulation::test_result::error, "scenario couldn't be opened"); + return true; + } Global.SceneryFile.clear(); Application.pop_mode(); Application.push_mode(eu07_application::mode::launcher); @@ -73,6 +78,11 @@ bool scenarioloader_mode::update() { // back to the launcher as when the scenario can't be opened, not on to the driver mode with the scenery half loaded ErrorLog( "Bad init: scenario loading failed" ); state.reset(); + if( Global.test.enabled ) { + // there's nothing to test; the exit code says the run went wrong + simulation::Test.finish( simulation::test_result::error, "scenario loading failed" ); + return true; + } Global.SceneryFile.clear(); Application.pop_mode(); Application.push_mode(eu07_application::mode::launcher); diff --git a/rendering/nullrenderer.cpp b/rendering/nullrenderer.cpp index 332d357c..f193ad3e 100644 --- a/rendering/nullrenderer.cpp +++ b/rendering/nullrenderer.cpp @@ -1,6 +1,25 @@ #include "stdafx.h" #include "rendering/nullrenderer.h" +#include "application/application.h" + +void null_imgui_renderer::BeginFrame() +{ + auto &io { ImGui::GetIO() }; + if( false == io.Fonts->IsBuilt() ) { + unsigned char *pixels; + int width, height; + io.Fonts->GetTexDataAsAlpha8( &pixels, &width, &height ); + } +} + +bool null_renderer::Render() +{ + // the ui frame begun by the application has to be ended, or imgui won't start the next one + Application.render_ui(); + return true; +} + std::unique_ptr null_renderer::create_func() { return std::make_unique(); diff --git a/rendering/nullrenderer.h b/rendering/nullrenderer.h index 0b21580b..2a7ef039 100644 --- a/rendering/nullrenderer.h +++ b/rendering/nullrenderer.h @@ -35,7 +35,18 @@ private: release_() override { /* null implementation, nothing to do */ } }; +// user interface backend which draws nothing: imgui still builds its frames, so the ui code runs as usual +class null_imgui_renderer : public imgui_renderer { +public: + bool Init() override { return true; } + void Shutdown() override { /* null implementation, nothing to do */ } + // imgui wants its font atlas built before a frame starts; that's done on the cpu, no texture is made of it + void BeginFrame() override; + void Render() override { /* null implementation, nothing to do */ } +}; + // bare-bones render controller, in lack of anything better yet +// it's the renderer of the test mode (--test): models are loaded for the physics, textures aren't class null_renderer : public gfx_renderer { public: @@ -49,7 +60,7 @@ public: Init( GLFWwindow *Window ) override { return true; } // main draw call. returns false on error bool - Render() override { return true; } + Render() override; void SwapBuffers() override { /* null implementation, nothing to do */ } inline @@ -105,20 +116,22 @@ public: void Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) override { /* null implementation, nothing to do */ } IMaterial const * - Material( material_handle const Material ) const override { return m_materials.at(Material - 1).get(); } + Material( material_handle const Material ) const override { + return ( Material > 0 && static_cast( Material ) <= m_materials.size() ? m_materials[ Material - 1 ].get() : IMaterial::null_material() ); } // shader methods auto Fetch_Shader( std::string const &name ) -> std::shared_ptr override { throw std::runtime_error("not impl"); } // texture methods + // no textures are loaded; whoever asks gets the null texture texture_handle - Fetch_Texture( std::string const &Filename, bool const Loadnow = true, GLint format_hint = GL_SRGB_ALPHA ) override { throw std::runtime_error("not impl"); } + Fetch_Texture( std::string const &Filename, bool const Loadnow = true, GLint format_hint = GL_SRGB_ALPHA ) override { return null_handle; } void Bind_Texture( texture_handle const Texture ) override { /* null implementation, nothing to do */ } void Bind_Texture( std::size_t const Unit, texture_handle const Texture ) override { /* null implementation, nothing to do */ } - opengl_texture & - Texture( texture_handle const Texture ) override { throw std::runtime_error("not impl"); } - opengl_texture const & - Texture( texture_handle const Texture ) const override { throw std::runtime_error("not impl"); } + ITexture & + Texture( texture_handle const Texture ) override { return *ITexture::null_texture(); } + ITexture const & + Texture( texture_handle const Texture ) const override { return *ITexture::null_texture(); } // utility methods void Pick_Control_Callback( std::function Callback ) override { /* null implementation, nothing to do */ } @@ -152,11 +165,12 @@ public: imgui_renderer *GetImguiRenderer() override { - return nullptr; + return &m_imgui; } private: std::string empty_str; + null_imgui_renderer m_imgui; gfx::geometrybank_manager m_geometry; std::vector> m_materials; }; diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index ea217717..59635c38 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "simulation/simulationtime.h" #include "simulation/simulationenvironment.h" +#include "simulation/simulationtest.h" #include "utilities/Globals.h" #include "world/Event.h" @@ -109,6 +110,8 @@ state_manager::init_scripting_interface() { simulation::Memory.insert( memorycell ); simulation::Region->insert( memorycell ); } + // the cell a test scenery reports its result to + simulation::Test.init_result_cell(); } // legacy method, calculates changes in simulation state over specified time diff --git a/simulation/simulationtest.cpp b/simulation/simulationtest.cpp new file mode 100644 index 00000000..4dbe4c24 --- /dev/null +++ b/simulation/simulationtest.cpp @@ -0,0 +1,149 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "simulation/simulationtest.h" + +#include "application/application.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "vehicle/DynObj.h" +#include "world/Event.h" +#include "world/MemCell.h" + +namespace simulation { + +test_manager Test; + +std::string_view to_string(test_result const Result) +{ + switch (Result) + { + case test_result::pass: return "pass"; + case test_result::fail: return "fail"; + case test_result::signal_passed_at_danger: return "signal passed at danger"; + case test_result::timeout: return "timeout"; + case test_result::standstill: return "standstill"; + case test_result::error: return "error"; + } + return "unknown"; +} + +void test_manager::init_result_cell() +{ + auto *cell = new TMemCell({0, -1, std::string(result_cell), "memcell"}); + // neither of the reported values, so that a cell nobody wrote to says nothing + cell->UpdateValues("", -1.0, 0.0, basic_event::flags::value1 | basic_event::flags::value2); + simulation::Memory.insert(cell); + simulation::Region->insert(cell); +} + +void test_manager::start() +{ + if (m_started) + return; + m_started = true; + m_time = 0.0; + m_resttime = 0.0; + if (simulation::Vehicles.sequence().empty()) + { + // most likely the scenery wasn't found, or it isn't a test scenery + finish(test_result::error, "the scenery has no vehicles"); + return; + } + // a pause set by the scenery or the settings would never be lifted, nobody is there to press the key + Global.iPause = 0; + WriteLog(std::format("test: started, timeout {:.0f} s, standstill {:.0f} s{}{}", Global.test.timeout, Global.test.standstill, + Global.test.pass_on_standstill ? ", standstill passes" : "", + Global.test.fail_if_stop_overran ? "" : ", signals passed at danger allowed")); +} + +void test_manager::update(double const Deltatime) +{ + if (false == m_started || m_finished) + return; + + m_time += Deltatime; + + if (auto const *cell = simulation::Memory.find(std::string(result_cell))) + { + auto const value { cell->Value1() }; + auto const reason { cell->Text().empty() ? std::string() : " (" + cell->Text() + ")" }; + if (value == 0.0) + { + finish(test_result::fail, "__test.result set to 0" + reason); + return; + } + if (value == 1.0 && false == m_passrequested) + { + m_passrequested = true; + WriteLog(std::format("test: pass requested at {:.1f} s{}, waiting for every vehicle to stop", m_time, reason)); + } + } + + auto const atrest = std::none_of(simulation::Vehicles.sequence().begin(), simulation::Vehicles.sequence().end(), + [](TDynamicObject const *Vehicle) { return std::abs(Vehicle->GetVelocity()) > 0.01; }); + m_resttime = atrest ? m_resttime + Deltatime : 0.0; + + if (m_passrequested && m_resttime >= pass_settle_time) + { + finish(test_result::pass, "__test.result set to 1 and every vehicle stopped"); + return; + } + if (Global.test.standstill > 0.0 && m_resttime >= Global.test.standstill) + { + auto const reason { std::format("every vehicle stood still for {:.0f} s", Global.test.standstill) }; + finish(Global.test.pass_on_standstill ? test_result::pass : test_result::standstill, reason); + return; + } + if (Global.test.timeout > 0.0 && m_time >= Global.test.timeout) + { + finish(test_result::timeout, std::format("no result after {:.0f} s", Global.test.timeout)); + return; + } +} + +void test_manager::signal_passed_at_danger(std::string const &Vehicle, std::string const &Signal) +{ + auto const reason { Vehicle + " passed signal " + Signal + " at danger" }; + if (false == Global.test.enabled) + { + WriteLog(reason, logtype::generic); + return; + } + if (false == Global.test.fail_if_stop_overran) + { + WriteLog(std::format("test: {} at {:.1f} s, allowed by test.fail_if_stop_overran", reason, m_time)); + return; + } + finish(test_result::signal_passed_at_danger, reason); +} + +void test_manager::finish(test_result const Result, std::string const &Reason) +{ + if (m_finished || false == Global.test.enabled) + return; + m_finished = true; + m_result = Result; + + auto const line { std::format("test: {} (exit code {}) after {:.1f} s of simulation time: {}", to_string(Result), static_cast(Result), m_time, Reason) }; + if (Result == test_result::pass) + WriteLog(line); + else + ErrorLog(line); + // the process has no console of its own; whoever started it with a redirected output gets the verdict there too + std::fprintf(stdout, "%s\n", line.c_str()); + std::fflush(stdout); + + Application.queue_quit(true); +} + +} // namespace simulation diff --git a/simulation/simulationtest.h b/simulation/simulationtest.h new file mode 100644 index 00000000..c9bc5437 --- /dev/null +++ b/simulation/simulationtest.h @@ -0,0 +1,66 @@ +/* +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 + +// integration test mode (eu07 --test -s , eu07/maszyna#541): the scenery runs unattended in a tight +// fixed-step loop and the simulator quits with an exit code saying how the test went +namespace simulation { + +// exit codes of a test run; public, scripts and CI read them +enum class test_result : int { + pass = 0, // __test.result set to 1, then every vehicle came to rest (or test.pass_on_standstill) + fail = 1, // __test.result set to 0 + signal_passed_at_danger = 2, // a train went past a stop signal (SetVelocity 0 0), unless test.fail_if_stop_overran no + timeout = 3, // test.timeout seconds of simulation time went by without a result + standstill = 4, // every vehicle stood still for test.standstill seconds without a result + error = 5, // the run could not start (no -s, the scenery failed to load) or the simulator threw +}; + +std::string_view to_string(test_result Result); + +class test_manager { + +public: + // name of the memory cell a scenery reports its result to + static constexpr std::string_view result_cell { "__test.result" }; + // how long everything has to stand before a requested pass is granted [s] + static constexpr double pass_settle_time { 1.0 }; + // fixed simulation time step of one frame in test mode [s]: 3 physics steps of 10 ms + static constexpr double frame_step { 0.03 }; + + // creates the result cell; called with the other scripting interface cells, before the scenery is read + void init_result_cell(); + // the simulation is ready: start the clocks + void start(); + // checks the end conditions after a simulation frame of Deltatime seconds + void update(double Deltatime); + // reported by a driver whose train went past a signal showing stop + void signal_passed_at_danger(std::string const &Vehicle, std::string const &Signal); + // ends the test with given result; the first call wins + void finish(test_result Result, std::string const &Reason); + + bool finished() const { return m_finished; } + // exit code of the process: the result, or error while the test hasn't finished + int exit_code() const { return static_cast(m_finished ? m_result : test_result::error); } + +private: + bool m_started { false }; + bool m_finished { false }; + test_result m_result { test_result::error }; + bool m_passrequested { false }; + double m_time { 0.0 }; // simulation time since the start [s] + double m_resttime { 0.0 }; // how long every vehicle has been standing [s] +}; + +extern test_manager Test; + +} // namespace simulation diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index b4f2e14b..e3622317 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -456,6 +456,40 @@ bool global_settings::ConfigParseInput(cParser& Parser, const std::string& token return false; } +bool global_settings::ConfigParseTest(cParser& Parser, const std::string& token) +{ + // integration test limits, usually given in the config block of a test scenery + if (token == "test.timeout") + { + double value{ test.timeout }; + ParseOne(Parser, value); + if (false == test.timeout_from_command_line) + test.timeout = std::max(0.0, value); + return true; + } + + if (token == "test.standstill") + { + ParseOne(Parser, test.standstill); + test.standstill = std::max(0.0, test.standstill); + return true; + } + + if (token == "test.pass_on_standstill") + { + ParseOne(Parser, test.pass_on_standstill); + return true; + } + + if (token == "test.fail_if_stop_overran") + { + ParseOne(Parser, test.fail_if_stop_overran); + return true; + } + + return false; +} + bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string& token) { if (token == "async.trainThreads") @@ -1316,6 +1350,9 @@ void global_settings::ConfigParse(cParser& Parser) if (ConfigParseDebug(Parser, token)) continue; + if (ConfigParseTest(Parser, token)) + continue; + // WriteLog(std::format("Unknown config token: {}", token), logtype::warning, false); } while (true); diff --git a/utilities/Globals.h b/utilities/Globals.h index ad216cd9..8fa04e9c 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -86,6 +86,15 @@ struct global_settings { std::string local_start_vehicle{ "EU07-424" }; std::string editor_enter_vehicle; // vehicle placed in the editor, to be entered when the driving goes on bool editor_session{ false }; // scenery was opened for editing (-edit): loader keeps track of scenery layers and hands over to the editor + // integration test mode (--test, eu07/maszyna#541); the limits can be set in the scenery's config block (test.*) + struct test_settings { + bool enabled{ false }; + double timeout{ 3600.0 }; // [s] of simulation time; no result by then fails the test + double standstill{ 300.0 }; // [s] every vehicle standing still this long ends the test; 0 = never + bool pass_on_standstill{ false }; // the standstill end passes the test instead of failing it + bool fail_if_stop_overran{ true }; // a train passing a signal at stop ends the test as failed; with false it's only logged + bool timeout_from_command_line{ false }; // --test-timeout given, the scenery doesn't override it + } test; bool editor_tracks{ false }; // F3 track workspace: draw and pick tracks without the scenery draw-range cap int iConvertModels{ 0 }; // tworzenie plików binarnych int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan @@ -377,6 +386,7 @@ struct global_settings { bool ConfigParseNetwork(cParser& Parser, const std::string& token); bool ConfigParseHardware(cParser& Parser, const std::string& token); bool ConfigParseDebug(cParser& Parser, const std::string& token); + bool ConfigParseTest(cParser& Parser, const std::string& token); bool ConfigParse_gfx( cParser &parser, std::string_view const Token ); // sends basic content of the class in legacy (text) format to provided stream void diff --git a/utilities/Timer.cpp b/utilities/Timer.cpp index 0b877f2b..388330bb 100644 --- a/utilities/Timer.cpp +++ b/utilities/Timer.cpp @@ -26,6 +26,7 @@ double fRenderTime{ 0.0 }; double fSoundTimer{ 0.0 }; double fSinceStart{ 0.0 }; double override_delta = -1.0f; +double fixed_step = 0.0; double GetTime() { @@ -53,6 +54,11 @@ void set_delta_override(double t) override_delta = t; } +void set_fixed_step(double t) +{ + fixed_step = t; +} + void ResetTimers() { UpdateTimers( Global.iPause != 0 ); @@ -76,10 +82,15 @@ void UpdateTimers(bool pause) fr = 1000000000; #endif DeltaRenderTime = double(count - oldCount) / double(fr); + if (fixed_step > 0.0) + { + // the frame lasts exactly as long as said, however long it took to compute; the time speed doesn't apply + DeltaRenderTime = fixed_step; + } fRenderTime += DeltaRenderTime; if (!pause) { - DeltaTime = Global.fTimeSpeed * DeltaRenderTime; + DeltaTime = (fixed_step > 0.0 ? 1.0 : Global.fTimeSpeed) * DeltaRenderTime; fSoundTimer += DeltaTime; if (fSoundTimer > 0.1) fSoundTimer = 0.0; diff --git a/utilities/Timer.h b/utilities/Timer.h index 32e170a4..15fdd4f6 100644 --- a/utilities/Timer.h +++ b/utilities/Timer.h @@ -19,6 +19,9 @@ double GetDeltaRenderTime(); void set_delta_override(double v); +// every frame advances the clocks by this many seconds, whatever the real time; 0 = real time (test mode) +void set_fixed_step(double v); + void ResetTimers(); void UpdateTimers(bool pause); diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index c5f5f51e..5f1bde20 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include "world/MemCell.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" +#include "simulation/simulationtest.h" #include "world/Track.h" #include "world/station.h" #include "input/keyboardinput.h" @@ -767,7 +768,12 @@ void TController::TableCheck(double fDistance) { // aktualizacja rekordów z wyjątkiem ostatniego if (sSpeedTable[i].iFlags & spEnabled) // jeśli pozycja istotna { - if (sSpeedTable[i].Update()) + auto const waspassed { TestFlag( sSpeedTable[ i ].iFlags, spElapsed ) }; + auto const switchchanged { sSpeedTable[ i ].Update() }; + if( false == waspassed ) { + CheckSignalPassedAtDanger( sSpeedTable[ i ] ); + } + if (switchchanged) { if( Global.iWriteLogEnabled & 8 ) { WriteLog( "Speed table for " + OwnerName() + " detected switch change at " + sSpeedTable[ i ].trTrack->name() + " (generating fresh trace)" ); @@ -816,7 +822,12 @@ void TController::TableCheck(double fDistance) } } } - sSpeedTable[iLast].Update(); // aktualizacja ostatniego + // aktualizacja ostatniego; a signal at stop often is the last, the scan doesn't go past it + auto const lastwaspassed { TestFlag( sSpeedTable[ iLast ].iFlags, spElapsed ) }; + sSpeedTable[iLast].Update(); + if( false == lastwaspassed ) { + CheckSignalPassedAtDanger( sSpeedTable[ iLast ] ); + } // WriteLog("TableCheck: Upate last track. Dist=" + AnsiString(sSpeedTable[iLast].fDist)); if( sSpeedTable[ iLast ].fDist < fDistance ) { TableTraceRoute( fDistance, pVehicles[ end::rear ] ); // doskanowanie dalszego odcinka @@ -827,6 +838,20 @@ void TController::TableCheck(double fDistance) } }; +void TController::CheckSignalPassedAtDanger( TSpeedPos const &Point ) const +{ + if( false == TestFlag( Point.iFlags, spElapsed ) + || false == TestFlag( Point.iFlags, spEvent ) + || false == TestFlag( Point.iFlags, spSemaphor ) + || true == TestFlag( Point.iFlags, spStopOnSBL ) // SetVelocity 0 x on automatic block: may go on after a stop + || Point.fVelNext != 0.0 + || Point.evEvent == nullptr + || ( mvOccupied->CategoryFlag & 1 ) == 0 ) { // railway only + return; + } + simulation::Test.signal_passed_at_danger( OwnerName(), Point.evEvent->m_name ); +} + auto const passengerstopmaxdistance { 400.0 }; // maximum allowed distance between passenger stop point and consist head TCommandType TController::TableUpdate(double &fVelDes, double &fDist, double &fNext, double &fAcc) diff --git a/vehicle/Driver.h b/vehicle/Driver.h index f88e115a..8c12087f 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -519,6 +519,8 @@ private: bool TableNotFound( basic_event const *Event, double const Distance ) const; void TableTraceRoute( double fDistance, TDynamicObject *pVehicle ); void TableCheck( double fDistance ); + // reports the train going past a signal showing stop (SetVelocity 0 0); called once the head passed Point + void CheckSignalPassedAtDanger( TSpeedPos const &Point ) const; TCommandType TableUpdate( double &fVelDes, double &fDist, double &fNext, double &fAcc ); bool TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, double const Signaldistance ); bool TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPos &Point, double &Signaldistance, int const Pointindex );