From 5fa03304eff805ca5bbe4dce8dfeebdd40c0250a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 00:45:46 +0100 Subject: [PATCH 01/32] fix(ui): keep panels in the main window when resizing them at the screen edge Pulling a panel's grip into the corner of the screen pushed it a couple of pixels past the main window, and ImGui gave it an OS window of its own. Now a panel only comes out when dragged with the cursor outside the main window. A panel that stays in the main window needs its grips in reach, so a resize stops at the edge, and a panel left past the edge or too big for the window is moved back and shrunk to fit. This is a new io.ConfigViewportsNoAutoDetach option in our copy of ImGui, so it has to be carried over when ImGui gets updated. ImGui itself has no option for this, as of 1.93.1. --- application/uilayer.cpp | 2 ++ imgui/imgui.cpp | 58 +++++++++++++++++++++++++++++++++++++++-- imgui/imgui.h | 1 + 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 2dd771bdd..893b84e92 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -403,6 +403,8 @@ bool ui_layer::init(GLFWwindow *Window) { m_imguiio->ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; m_imguiio->ConfigViewportsNoTaskBarIcon = true; + // only dragging a panel with the cursor past the main window takes it out, not resizing it or placing it at the edge + m_imguiio->ConfigViewportsNoAutoDetach = true; auto &platformio{ImGui::GetPlatformIO()}; imgui_create_window = platformio.Platform_CreateWindow; platformio.Platform_CreateWindow = create_viewport_window; diff --git a/imgui/imgui.cpp b/imgui/imgui.cpp index 573efe244..a9026b408 100644 --- a/imgui/imgui.cpp +++ b/imgui/imgui.cpp @@ -1294,6 +1294,7 @@ static void WindowSelectViewport(ImGuiWindow* window); static void WindowSyncOwnedViewport(ImGuiWindow* window, ImGuiWindow* parent_window_in_stack); static bool UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImGuiViewportP* host_viewport); static bool UpdateTryMergeWindowIntoHostViewports(ImGuiWindow* window); +static bool WindowStaysInHostViewport(ImGuiWindow* window, ImGuiViewportP* viewport); // MaSzyna addition static bool GetWindowAlwaysWantOwnViewport(ImGuiWindow* window); static int FindPlatformMonitorForPos(const ImVec2& pos); static int FindPlatformMonitorForRect(const ImRect& r); @@ -1489,6 +1490,7 @@ ImGuiIO::ImGuiIO() ConfigViewportsNoTaskBarIcon = false; ConfigViewportsNoDecoration = true; ConfigViewportsNoDefaultParent = false; + ConfigViewportsNoAutoDetach = false; // Miscellaneous options MouseDrawCursor = false; @@ -6845,6 +6847,11 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& si ImVec2 pos_target(FLT_MAX, FLT_MAX); ImVec2 size_target(FLT_MAX, FLT_MAX); + // MaSzyna addition: a resize stops at the edge of the host viewport, so the grips and borders can still be reached + ImRect resize_limit_rect(-FLT_MAX, -FLT_MAX, +FLT_MAX, +FLT_MAX); + if (!window->ViewportOwned && WindowStaysInHostViewport(window, window->Viewport)) + resize_limit_rect = window->Viewport->GetWorkRect(); + // Clip mouse interaction rectangles within the viewport rectangle (in practice the narrowing is going to happen most of the time). // - Not narrowing would mostly benefit the situation where OS windows _without_ decoration have a threshold for hovering when outside their limits. // This is however not the case with current backends under Win32, but a custom borderless window implementation would benefit from it. @@ -6892,6 +6899,7 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& si ImVec2 clamp_max = ImVec2(def.CornerPosN.x == 0.0f ? clamp_rect.Max.x : +FLT_MAX, def.CornerPosN.y == 0.0f ? clamp_rect.Max.y : +FLT_MAX); ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN); // Corner of the window corresponding to our corner grip corner_target = ImClamp(corner_target, clamp_min, clamp_max); + corner_target = ImClamp(corner_target, resize_limit_rect.Min, resize_limit_rect.Max); CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); } @@ -6971,6 +6979,7 @@ static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& si ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX); ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX); border_target = ImClamp(border_target, clamp_min, clamp_max); + border_target[axis] = ImClamp(border_target[axis], resize_limit_rect.Min[axis], resize_limit_rect.Max[axis]); if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent { ImGuiWindow* parent_window = window->ParentWindow; @@ -7056,6 +7065,30 @@ static inline void ClampWindowPos(ImGuiWindow* window, const ImRect& visibility_ window->Pos = ImClamp(window->Pos, visibility_rect.Min - size_for_clamping, visibility_rect.Max); } +// MaSzyna addition: see io.ConfigViewportsNoAutoDetach +static inline bool IsWindowBeingMoved(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return g.MovingWindow != NULL && g.MovingWindow->RootWindowDockTree == window->RootWindowDockTree; +} + +// MaSzyna addition: a window left past the edge of its host viewport would have its grips and borders out of reach +static inline void KeepWindowInsideRect(ImGuiWindow* window, const ImRect& rect) +{ + if (rect.GetWidth() <= 0.0f || rect.GetHeight() <= 0.0f) + return; + + if (!(window->Flags & (ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize))) + { + const ImVec2 size_full = ImMin(window->SizeFull, rect.GetSize()); + if (size_full.x != window->SizeFull.x || size_full.y != window->SizeFull.y) + ImGui::MarkIniSettingsDirty(window); + window->SizeFull = size_full; + window->Size = ImMin(window->Size, rect.GetSize()); + } + window->Pos = ImMax(ImMin(window->Pos, rect.Max - window->Size), rect.Min); +} + static void RenderWindowOuterSingleBorder(ImGuiWindow* window, int border_n, ImU32 border_col, float border_size) { const ImGuiResizeBorderDef& def = resize_border_def[border_n]; @@ -7828,7 +7861,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Late create viewport if we don't fit within our current host viewport. if (window->ViewportAllowPlatformMonitorExtend >= 0 && !window->ViewportOwned && !(window->Viewport->Flags & ImGuiViewportFlags_IsMinimized)) - if (!window->Viewport->GetMainRect().Contains(window->Rect())) + if (!window->Viewport->GetMainRect().Contains(window->Rect()) && !WindowStaysInHostViewport(window, window->Viewport)) { // This is based on the assumption that the DPI will be known ahead (same as the DPI of the selection done in UpdateSelectWindowViewport) //ImGuiViewport* old_viewport = window->Viewport; @@ -7878,6 +7911,11 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) ClampWindowPos(window, visibility_rect); } } + + // MaSzyna addition: the host window of a floating dock node has its position set through the api, it's free to move all the same + if (!window->ViewportOwned && !IsWindowBeingMoved(window) && (!window_pos_set_by_api || (flags & ImGuiWindowFlags_DockNodeHost)) && WindowStaysInHostViewport(window, window->Viewport)) + KeepWindowInsideRect(window, viewport_work_rect); + window->Pos = ImTrunc(window->Pos); // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) @@ -15780,6 +15818,22 @@ static bool ImGui::GetWindowAlwaysWantOwnViewport(ImGuiWindow* window) return false; } +// MaSzyna addition: see io.ConfigViewportsNoAutoDetach +static bool ImGui::WindowStaysInHostViewport(ImGuiWindow* window, ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + if (!g.IO.ConfigViewportsNoAutoDetach) + return false; + + const ImGuiWindowFlags not_affected_flags = ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip; + if (window->Flags & not_affected_flags) + return false; + + if (!IsWindowBeingMoved(window) || !IsMousePosValid()) + return true; + return viewport->GetMainRect().Contains(g.IO.MousePos); +} + static bool ImGui::UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImGuiViewportP* viewport) { ImGuiContext& g = *GImGui; @@ -16323,7 +16377,7 @@ static void ImGui::WindowSelectViewport(ImGuiWindow* window) // Fallback: merge in default viewport if z-order matches, otherwise create a new viewport if (window->Viewport == NULL) if (!UpdateTryMergeWindowIntoHostViewport(window, main_viewport)) - window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + window->Viewport = WindowStaysInHostViewport(window, main_viewport) ? main_viewport : AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); // Mark window as allowed to protrude outside of its viewport and into the current monitor if (!lock_viewport) diff --git a/imgui/imgui.h b/imgui/imgui.h index bafe97ff8..6bbc42dec 100644 --- a/imgui/imgui.h +++ b/imgui/imgui.h @@ -2360,6 +2360,7 @@ struct ImGuiIO bool ConfigViewportsNoTaskBarIcon; // = false // Disable default OS task bar icon flag for secondary viewports. When a viewport doesn't want a task bar icon, ImGuiViewportFlags_NoTaskBarIcon will be set on it. bool ConfigViewportsNoDecoration; // = true // Disable default OS window decoration flag for secondary viewports. When a viewport doesn't want window decorations, ImGuiViewportFlags_NoDecoration will be set on it. Enabling decoration can create subsequent issues at OS levels (e.g. minimum window size). bool ConfigViewportsNoDefaultParent; // = false // Disable default OS parenting to main viewport for secondary viewports. By default, viewports are marked with ParentViewportId = , expecting the platform backend to setup a parent/child relationship between the OS windows (some backend may ignore this). Set to true if you want the default to be 0, then all viewports will be top-level OS windows. + bool ConfigViewportsNoAutoDetach; // = false // MaSzyna addition: a floating window stays inside its host viewport: a resize stops at its edge, and a window left past it, or too big for it, is moved and shrunk to fit. It gets a viewport of its own only when dragged with the mouse cursor past the host viewport. Popups and tooltips are not affected. // Miscellaneous options // (you can visualize and interact with all options in 'Demo->Configuration') From 9364b03e1ad9d9711942864c4d5ab17a00f487d3 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH 02/32] fix(track): what Sonar reports on the unevenness The blockers are in the EP brake of a vehicle, older than this work: its arrays made with new[] are vectors now, which also removes the deletes. - The caches of the unevenness of a track are no longer mutable: what fills them isn't const any more. - TSegment gets back to RaPositionGet in one piece, with what the path has to do with the others in one struct. - Less nesting in the group of switches and in the fields of the editor, nested conditionals written out, and the attribute is read from a string_view. A test no longer counts in floating point. No change in behaviour. --- application/editormodetrack.cpp | 33 ++++----- tests/unit/test_track_uneven.cpp | 33 ++++++--- vehicle/DynObj.cpp | 73 +++++++++---------- vehicle/DynObj.h | 2 +- world/Segment.cpp | 77 +++++++++----------- world/Segment.h | 29 ++++---- world/Track.h | 21 +++--- world/TrackUneven.cpp | 117 ++++++++++++++++++------------- world/TrackUneven.h | 45 +++++++----- 9 files changed, 223 insertions(+), 207 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 49059c213..0ae732d86 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1166,26 +1166,23 @@ void editor_mode::render_path_parameters(TTrack &Track) track->m_uneven.value = std::clamp(track->m_uneven.value, 0.f, track_uneven::most); item_tooltip("Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values"); finish_edit(rebuild::parameters); - struct component - { - char const *label; - float *value; - bool shown; - }; - auto const plain{track->eType == tt_Normal}; - for (auto const &part : {component{"Up and down (mm)", &track->m_uneven.up, true}, - component{"Sideways (mm)", &track->m_uneven.side, true}, - component{"Cross level (mm)", &track->m_uneven.cross, plain}, - component{"Diverging path (mm)", &track->m_uneven.branch, !plain}, - component{"Diverging, up and down (mm)", &track->m_uneven.branchup, !plain}, - component{"Diverging, sideways (mm)", &track->m_uneven.branchside, !plain}}) - { - if (false == part.shown) - continue; - if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) - *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); + auto const component = [&](char const *Label, float &Value) { + if (ImGui::InputFloat(STR_C(Label), &Value, 0.0f, 0.0f, "%.1f")) + Value = Value < 0.f ? -1.f : std::min(Value, track_uneven::most); item_tooltip("Set apart from the unevenness, mm. Negative: the unevenness"); finish_edit(rebuild::parameters); + }; + component("Up and down (mm)", track->m_uneven.up); + component("Sideways (mm)", track->m_uneven.side); + if (track->eType == tt_Normal) + { + component("Cross level (mm)", track->m_uneven.cross); + } + else + { + component("Diverging path (mm)", track->m_uneven.branch); + component("Diverging, up and down (mm)", track->m_uneven.branchup); + component("Diverging, sideways (mm)", track->m_uneven.branchside); } } if (track->eType == tt_Normal && (track->iCategoryFlag & 15) == 1) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 65b7da6e1..06f4663b0 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -2,6 +2,8 @@ #include "doctest.h" #include "world/TrackUneven.h" +#include +#include #include #include @@ -12,19 +14,20 @@ double deviation(std::vector const &Values) auto mean{0.0}; for (auto const value : Values) mean += value; - mean /= Values.size(); + auto const count{static_cast(Values.size())}; + mean /= count; auto sum{0.0}; for (auto const value : Values) sum += (value - mean) * (value - mean); - return std::sqrt(sum / Values.size()); + return std::sqrt(sum / count); } double peak(std::vector Values) { for (auto &value : Values) value = std::abs(value); - std::sort(Values.begin(), Values.end()); - return Values[static_cast(0.999 * Values.size())]; + std::ranges::sort(Values); + return Values[Values.size() * 999 / 1000]; } void add_chord(std::vector &Output, std::vector const &Line, int const Half) @@ -44,14 +47,22 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") int const steps{4000}; int const base{10}; // 5 m int const lines{36}; - double vertical{0.0}, horizontal{0.0}, twist{0.0}, wander{0.0}, level{0.0}, j{0.0}, away{0.0}; + double vertical{0.0}; + double horizontal{0.0}; + double twist{0.0}; + double wander{0.0}; + double level{0.0}; + double j{0.0}; + double away{0.0}; for (int line = 0; line < lines; ++line) { auto const heading{glm::pi() * (line + 0.37) / lines}; glm::dvec3 const start{7919.0 * std::sin(line * 1.7), 0.0, 6577.0 * std::cos(line * 2.3)}; glm::dvec3 const along{std::cos(heading), 0.0, std::sin(heading)}; glm::dvec3 const across{-along.z, 0.0, along.x}; - std::vector up[2], side[2], middle; + std::array, 2> up; + std::array, 2> side; + std::vector middle; for (int i = 0; i < steps; ++i) { auto const point{start + along * (i * step)}; @@ -64,7 +75,11 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") } middle.push_back(track_uneven::field(point).y * amplitude); } - std::vector verticals, horizontals, twists, gauges, levels; + std::vector verticals; + std::vector horizontals; + std::vector twists; + std::vector gauges; + std::vector levels; for (int rail = 0; rail < 2; ++rail) { add_chord(verticals, up[rail], base); @@ -269,9 +284,9 @@ TEST_CASE("uneven track: the value along a track meets its neighbours exactly") CHECK(track_uneven::along(18.0, 35.0, 35.0, 0.25, 0.5) == doctest::Approx(35.0).epsilon(0.01)); // no jump anywhere auto last{track_uneven::along(18.0, 35.0, 0.0, 0.0, length)}; - for (auto distance{0.25}; distance <= length; distance += 0.25) + for (int step = 1; step <= 2000; ++step) { - auto const value{track_uneven::along(18.0, 35.0, 0.0, distance, length)}; + auto const value{track_uneven::along(18.0, 35.0, 0.0, step * 0.25, length)}; REQUIRE(std::abs(value - last) < 0.5); last = value; } diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index ac87f395c..305e12020 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3382,14 +3382,12 @@ bool TDynamicObject::Update(double dt, double dt1) FzadED = 0; //pętla bezpieczeństwa - bez ED auto const FzadPN = Fzad - FrED * MoverParameters->MED_FrED_factor; //np = 0; - // BUG: likely memory leak, allocation per inner loop, deleted only once outside - // TODO: sort this shit out - bool* PrzekrF = new bool[np]; + std::vector PrzekrF( np ); float nPrzekrF = 0; bool test = true; - float* FzED = new float[np]; - float* FzEP = new float[np]; - float* FmaxEP = new float[np]; + std::vector FzED( np ); + std::vector FzEP( np ); + std::vector FmaxEP( np ); // 3. ustaw pojazdom sile hamowania ED // - proporcjonalnie do mozliwosci @@ -3548,11 +3546,6 @@ bool TDynamicObject::Update(double dt, double dt1) } - delete[] PrzekrF; - delete[] FzED; - delete[] FzEP; - delete[] FmaxEP; - MED_oldFED = FzadED; } @@ -3661,33 +3654,32 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - if( axle_passed_joint( axle, dDOMoveLen, directioninconsist ) ) { - if( MoverParameters->Vel > 0.1 ) { - // NOTE: for combined clatter sound we supply 1/100th of actual value, as the sound module converts does the opposite, converting received (typically) 0-1 values to 0-100 range - auto const frequency = true == axle.clatter.is_combined() ? MoverParameters->Vel * 0.01 : 1.0; - axle.clatter - .pitch( frequency ) - .gain( volume ) - .play(); - // crude bump simulation, drop down on even axles, move back up on - // the odd ones - // MoverParameters->AccVert += (MoverParameters->Vel*0.1f) * - double const headroom = std::clamp( - MoverParameters->Vmax - ( MoverParameters->Vel + MoverParameters->Vmax * 0.32f ), - 0.0, MoverParameters->Vmax ); - double const jolt = headroom * .05f * ( MyTrack->iDamageFlag * 0.25f ); - if (MyTrack->eType == tt_Normal) - { - MoverParameters->AccVert += std::clamp( jolt, 0.0, 4.0 ); - } - else if (MyTrack->eType == tt_Switch) - { - double const accHorizontal = std::clamp( jolt, 0.0, 1.0 ) * ((axleindex % 2) != 0 ? 1 : -1); - MoverParameters->AccS += accHorizontal; - MoverParameters->AccN += accHorizontal; - MoverParameters->AccVert += std::clamp( jolt, 0.0, 2.0 ); - } - } + if( axle_passed_joint( axle, dDOMoveLen, directioninconsist ) + && MoverParameters->Vel > 0.1 ) { + // NOTE: for combined clatter sound we supply 1/100th of actual value, as the sound module converts does the opposite, converting received (typically) 0-1 values to 0-100 range + auto const frequency = true == axle.clatter.is_combined() ? MoverParameters->Vel * 0.01 : 1.0; + axle.clatter + .pitch( frequency ) + .gain( volume ) + .play(); + // crude bump simulation, drop down on even axles, move back up on + // the odd ones + // MoverParameters->AccVert += (MoverParameters->Vel*0.1f) * + double const headroom = std::clamp( + MoverParameters->Vmax - ( MoverParameters->Vel + MoverParameters->Vmax * 0.32f ), + 0.0, MoverParameters->Vmax ); + double const jolt = headroom * .05f * ( MyTrack->iDamageFlag * 0.25f ); + if (MyTrack->eType == tt_Normal) + { + MoverParameters->AccVert += std::clamp( jolt, 0.0, 4.0 ); + } + else if (MyTrack->eType == tt_Switch) + { + double const accHorizontal = std::clamp( jolt, 0.0, 1.0 ) * ((axleindex % 2) != 0 ? 1 : -1); + MoverParameters->AccS += accHorizontal; + MoverParameters->AccN += accHorizontal; + MoverParameters->AccVert += std::clamp( jolt, 0.0, 2.0 ); + } } ++axleindex; } @@ -8072,12 +8064,11 @@ TDynamicObject::update_shake( double const Timedelta ) { } bool -TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) { +TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const { // by the outer axle on its side of the vehicle: the two can be on different tracks auto const &outer { Axle.offset >= 0.0 ? Axle0 : Axle1 }; - auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; - if( railphase >= 0.0 ) { + if( auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; railphase >= 0.0 ) { // the rails under the axle, which aren't always those under the track the vehicle is on auto const passed { Axle.atjoints && std::abs( railphase - Axle.distance ) > 0.5 * outer.GetTrack()->fSoundDistance }; Axle.distance = railphase; diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index b903f0874..4fee9778e 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -405,7 +405,7 @@ private: bool atjoints { false }; // distance comes from TTrack::rail_phase }; // whether the axle has just passed a rail joint. On a track with dipping joints it goes by them, elsewhere the axle counts a rail off itself - bool axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ); + bool axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const; struct powertrain_sounds { sound_source inverter { sound_placement::engine }; diff --git a/world/Segment.cpp b/world/Segment.cpp index 9c0e1077e..1aff4ae42 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -69,10 +69,8 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N // poprawienie przechyłki fRoll1 = static_cast(glm::radians(fNewRoll1)); // Ra: przeliczone jest bardziej przydatne do obliczeń fRoll2 = static_cast(glm::radians(fNewRoll2)); - m_designrolls[ 0 ] = fRoll1; - m_designrolls[ 1 ] = fRoll2; - m_joints[ 0 ].reset(); - m_joints[ 1 ].reset(); + m_setting.designrolls = { fRoll1, fRoll2 }; + m_setting.joints = {}; bCurve = bIsCurve; if (Global.bRollFix) { // Ra: poprawianie przechyłki @@ -122,7 +120,7 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N fStoop = std::atan2(Point2.y - Point1.y, fLength); // pochylenie toru prostego, żeby nie liczyć wielokrotnie fStep = fNewStep; - if( m_uneven && pOwner->eType != tt_Switch ) { + if( m_setting.uneven && pOwner->eType != tt_Switch ) { // dense enough for the waves; a switch keeps its division, the blades depend on it fStep = std::min( fStep, 2.0 ); } @@ -373,21 +371,6 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const }; */ -void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { - design_position( fDistance, position, rotation ); - if( false == m_uneven ) { - return; - } - // direction and slope stay as designed: the physics takes the curve radius and the grade from them - auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; - glm::dvec3 const across { -std::cos( rotation.z ), 0.0, -std::sin( rotation.z ) }; - auto const amount { pOwner->uneven_at( fDistance, m_path ) }; - rotation.x += static_cast( track_uneven::stray_per_mm * ( - amount.up * track_uneven::roll( position, across, gauge ) - + amount.cross * track_uneven::level( position, across ) / gauge ) ); - position += track_uneven::across_only( stray( position, amount, fDistance ), across ); -} - glm::dvec3 TSegment::stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const { auto const field { track_uneven::field( Point ) }; glm::dvec3 result { field.x * Amount.side, field.y * Amount.up, field.z * Amount.side }; @@ -397,30 +380,25 @@ glm::dvec3 TSegment::stray( glm::dvec3 const &Point, track_uneven::amounts const } glm::dvec3 TSegment::uneven_bend( double const Distance ) const { - if( false == m_uneven ) { + if( false == m_setting.uneven ) { return glm::dvec3 { 0.0 }; } // over about a bogie auto const reach { 1.5 }; - auto const strayed = [&]( double const At ) { - return stray( point_extended( At ), pOwner->uneven_at( At, m_path ), At ); }; + auto const strayed = [ this ]( double const At ) { + // past the ends of the path straight on: the polynomial would run off on a short curved one + auto const on { std::clamp( At, 0.0, fLength ) }; + auto const t { FastGetTFromS( on ) }; + auto point { FastGetPoint( t ) }; + if( auto const direction { FastGetDirection( t, 0.1 / fLength ) }; on != At && glm::length( direction ) > 0.0 ) { + point += direction * ( ( At - on ) / glm::length( direction ) ); + } + return stray( point, pOwner->uneven_at( At, m_setting.path ), At ); }; return ( strayed( Distance + reach ) - 2.0 * strayed( Distance ) + strayed( Distance - reach ) ) / ( reach * reach ); } -glm::dvec3 TSegment::point_extended( double const Distance ) const { - auto const on { std::clamp( Distance, 0.0, fLength ) }; - auto const t { FastGetTFromS( on ) }; - auto const point { FastGetPoint( t ) }; - if( on == Distance ) { - return point; - } - auto const direction { FastGetDirection( t, 0.1 / fLength ) }; - auto const size { glm::length( direction ) }; - return size > 0.0 ? point + direction * ( ( Distance - on ) / size ) : point; -} - void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const { - if( false == m_uneven ) { + if( false == m_setting.uneven ) { return; } // the rails settling apart turn about the middle of the track, what's outside them follows the rail; the foot of the ballast stays @@ -430,12 +408,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi if( share <= 0.0 ) { return; } - auto const amount { pOwner->uneven_at( Distance, m_path ) }; + auto const amount { pOwner->uneven_at( Distance, m_setting.path ) }; Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * amount.cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, amount, Distance ) }; } -void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { +void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { if (bCurve) { // można by wprowadzić uproszczony wzór dla okręgów płaskich auto const t = FastGetTFromS(fDistance); // aproksymacja dystansu na krzywej Beziera na parametr (t) @@ -458,6 +436,17 @@ void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm rotation.y = static_cast(fStoop); // pochylenie toru prostego rotation.z = static_cast(fDirection); // kierunek toru w planie } + if( false == m_setting.uneven ) { + return; + } + // direction and slope stay as designed: the physics takes the curve radius and the grade from them + auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; + glm::dvec3 const across { -std::cos( rotation.z ), 0.0, -std::sin( rotation.z ) }; + auto const amount { pOwner->uneven_at( fDistance, m_setting.path ) }; + rotation.x += static_cast( track_uneven::stray_per_mm * ( + amount.up * track_uneven::roll( position, across, gauge ) + + amount.cross * track_uneven::level( position, across ) / gauge ) ); + position += track_uneven::across_only( stray( position, amount, fDistance ), across ); }; void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const { @@ -523,9 +512,9 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, parallel1 = glm::vec3{ glm::dvec3{ FastGetPoint_1() - FastGetPoint_0() } }; } parallel1 = glm::normalize( parallel1 ); - if( iSkip == 0 && m_joints[ 0 ] ) { - pos1 = glm::vec3{ m_joints[ 0 ]->first - Origin }; - parallel1 = glm::vec3{ m_joints[ 0 ]->second }; + if( iSkip == 0 && m_setting.joints[ 0 ] ) { + pos1 = glm::vec3{ m_setting.joints[ 0 ]->first - Origin }; + parallel1 = glm::vec3{ m_setting.joints[ 0 ]->second }; } if( iEnd == 0 ) iEnd = iSegCount; @@ -572,9 +561,9 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, parallel2 = glm::vec3{ glm::dvec3{ FastGetPoint_1() - FastGetPoint_0() } }; } parallel2 = glm::normalize( parallel2 ); - if( i == iSegCount && m_joints[ 1 ] ) { - pos2 = glm::vec3{ m_joints[ 1 ]->first - Origin }; - parallel2 = glm::vec3{ m_joints[ 1 ]->second }; + if( i == iSegCount && m_setting.joints[ 1 ] ) { + pos2 = glm::vec3{ m_setting.joints[ 1 ]->first - Origin }; + parallel2 = glm::vec3{ m_setting.joints[ 1 ]->second }; } // TODO: refactor the loop, there's no need to calculate starting points for each segment when we can copy the end points of the previous one diff --git a/world/Segment.h b/world/Segment.h index 75e070152..099480e36 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -9,7 +9,9 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include +#include #include "utilities/Classes.h" #include "rendering/geometrybank.h" @@ -50,11 +52,14 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach - float m_designrolls[ 2 ] { 0.f, 0.f }; // as entered; the two above can differ at a joint - bool m_uneven { false }; // see TrackUneven.h - int m_path { 0 }; // which path of a switch - // where the path meets the next one: the middle of their ends and their common vector across - std::optional> m_joints[ 2 ]; + // how the path sits among the others + struct setting { + std::array designrolls { 0.f, 0.f }; // as entered; fRoll1 and fRoll2 can differ at a joint + bool uneven { false }; // see TrackUneven.h + int path { 0 }; // which path of a switch + // where the path meets the next one: the middle of their ends and their common vector across + std::array>, 2> joints; + } m_setting; double fLength { -1.0 }; // długość policzona std::vector fTsBuffer; // wartości parametru krzywej t dla równych odcinków s std::vector fTsSlope; // nachylenie dt/du (= dt/ds * fStep) w tych samych punktach @@ -79,9 +84,6 @@ class TSegment void settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const; // displacement of a point by the unevenness, at a distance along the path glm::dvec3 stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const; - // a point of the path at a distance, past its ends straight on: the polynomial would run off on a short curved path - glm::dvec3 point_extended( double const Distance ) const; - void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; public: bool bCurve = false; @@ -125,20 +127,17 @@ public: // call before Init, it sets the density of the geometry void set_uneven( bool const Uneven, int const Path = 0 ) { - m_uneven = Uneven; - m_path = Path; } - glm::dvec3 - end_point( int const End ) const { - return End == 0 ? Point1 : Point2; } + m_setting.uneven = Uneven; + m_setting.path = Path; } // a point of the path and the unit vector across it in the plan void plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const; float design_roll( int const End ) const { - return m_designrolls[ End ]; } + return m_setting.designrolls[ End ]; } void set_joint( int const End, std::optional> const &Joint, float const Roll ) { - m_joints[ End ] = Joint; + m_setting.joints[ End ] = Joint; ( End == 0 ? fRoll1 : fRoll2 ) = Roll; } // second derivative of the displacement along the path; times speed squared: acceleration of an axle glm::dvec3 diff --git a/world/Track.h b/world/Track.h index f5bb8c1b6..72cbd8847 100644 --- a/world/Track.h +++ b/world/Track.h @@ -349,15 +349,15 @@ public: track_uneven::amounts uneven_declared( int const Path = 0 ) const; bool is_uneven() const; // the unevenness at a distance along a path, meeting the tracks joined at its ends - track_uneven::amounts uneven_at( double const Distance, int const Path = 0 ) const; + track_uneven::amounts uneven_at( double const Distance, int const Path = 0 ); // how much of the displacement a point of the profile at the depth takes double uneven_bed_share( double const Depth ) const; // closes the gaps and steps of cant between the ends of joined paths void share_joints(); double joint_depth() const; // mm - double joint_dip( double const Distance ) const; // m + double joint_dip( double const Distance ); // m // distance from the rail joint behind; negative: the track has no joints that dip - double rail_phase( double const Distance ) const; + double rail_phase( double const Distance ); void ConnectionsLog(); bool DoubleSlip() const; static void fetch_default_profiles(); @@ -395,8 +395,8 @@ private: double way { 1.0 }; // direction of the path along the run int stamp { -1 }; }; - void settle_rail_run() const; - mutable rail_run m_railrun; + void settle_rail_run(); + rail_run m_railrun; // the value along each path of a switch: the lowest of that path in the switches joined, 0 next to a track with none. // And at the ends of the paths, by what meets there: the other path at the common end, the path of a switch joined struct switch_stray { @@ -404,15 +404,16 @@ private: std::array, 2> ends; int stamp { -1 }; }; - mutable switch_stray m_switchstray; - switch_stray const &switch_uneven() const; - void settle_switch_stray() const; + switch_stray m_switchstray; + switch_stray const &switch_uneven(); + void settle_switch_stray(); + void settle_switch_ends(); // the lowest value of the paths ending at the point track_uneven::amounts switch_paths_at( glm::dvec3 const &Point ) const; // what a plain track comes to at its end, where it meets the other one - track_uneven::amounts uneven_met( TTrack const *Other, glm::dvec3 const &Point ) const; + track_uneven::amounts uneven_met( TTrack *Other, glm::dvec3 const &Point ); // the value of a switch where a track joined to it meets it - track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ) const; + track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ); static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above void create_road_profile( gfx::vertex_array &Output, bool const Forcetransition = false ); void create_road_side_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const &Road, bool const Forcetransition = false ); diff --git a/world/TrackUneven.cpp b/world/TrackUneven.cpp index 152449ad2..78505ac3a 100644 --- a/world/TrackUneven.cpp +++ b/world/TrackUneven.cpp @@ -24,42 +24,55 @@ bool is_switch( TTrack const *Track ) { return Track != nullptr && Track->eType == tt_Switch && Track->SwitchExtension; } -std::array joined_to_switch( TTrack const &Switch ) { +glm::dvec3 end_point( TSegment const &Segment, int const End ) { + + return End == 0 ? Segment.FastGetPoint_0() : Segment.FastGetPoint_1(); +} + +std::array joined_to_switch( TTrack const &Switch ) { auto const &extension { *Switch.SwitchExtension }; return { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] }; } // the switch and the switches joined directly to it, and to those -std::vector switch_group( TTrack const &Switch ) { +std::vector switch_group( TTrack &Switch ) { - std::vector group { &Switch }; - for( std::size_t index = 0; index < group.size(); ++index ) { - for( auto const *other : joined_to_switch( *group[ index ] ) ) { - if( is_switch( other ) && std::find( group.begin(), group.end(), other ) == group.end() ) { + std::vector group { &Switch }; + std::vector pending { &Switch }; + while( false == pending.empty() ) { + auto const *track { pending.back() }; + pending.pop_back(); + for( auto *other : joined_to_switch( *track ) ) { + if( is_switch( other ) && std::ranges::find( group, other ) == group.end() ) { group.push_back( other ); + pending.push_back( other ); } } } return group; } +// a plain track with none of a component keeps the switch to its design in it, any other comes to the value of the switch +void keep_to_plain_track( std::array &Values, TTrack const &Plain ) { + + auto const theirs { Plain.uneven_declared() }; + for( auto &path : Values ) { + if( theirs.up <= 0.0 ) { path.up = 0.0; } + if( theirs.side <= 0.0 ) { path.side = 0.0; } + } +} + // the value along each path: the lowest of that path in the group, 0 in a component that a plain track joined has none of -std::array group_path_values( std::vector const &Group ) { +std::array group_path_values( std::vector const &Group ) { std::array values { Group.front()->uneven_declared( 0 ), Group.front()->uneven_declared( 1 ) }; for( auto const *track : Group ) { values[ 0 ] = track_uneven::least( values[ 0 ], track->uneven_declared( 0 ) ); values[ 1 ] = track_uneven::least( values[ 1 ], track->uneven_declared( 1 ) ); for( auto const *other : joined_to_switch( *track ) ) { - if( other == nullptr || is_switch( other ) ) { - continue; - } - // any other comes to the value of the switch - auto const theirs { other->uneven_declared() }; - for( auto &path : values ) { - if( theirs.up <= 0.0 ) { path.up = 0.0; } - if( theirs.side <= 0.0 ) { path.side = 0.0; } + if( other != nullptr && false == is_switch( other ) ) { + keep_to_plain_track( values, *other ); } } } @@ -74,7 +87,7 @@ track_uneven::amounts least_at_ends( TTrack const &Switch, glm::dvec3 const &Poi track_uneven::amounts result { track_uneven::most, track_uneven::most, track_uneven::most }; for( int path = 0; path < 2; ++path ) { for( int end = 0; end < 2; ++end ) { - if( glm::length( Switch.path_segment( path )->end_point( end ) - Point ) < switch_end_reach ) { + if( glm::length( end_point( *Switch.path_segment( path ), end ) - Point ) < switch_end_reach ) { result = track_uneven::least( result, Value( path, end ) ); found = true; } @@ -165,16 +178,16 @@ double TTrack::uneven_bed_share( double const Depth ) const { return track_uneven::bed_share( Depth, std::abs( track_rail_profile( m_profile1.second ).front().position.y ), fTexHeight1 ); } -track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) const { +track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) { if( eType == tt_Switch ) { auto const &stray { switch_uneven() }; return track_uneven::along( stray.paths[ Path ], stray.ends[ Path ][ 0 ], stray.ends[ Path ][ 1 ], Distance, path_segment( Path )->GetLength() ); } - return track_uneven::along( uneven_declared(), uneven_met( trPrev, Segment->end_point( 0 ) ), uneven_met( trNext, Segment->end_point( 1 ) ), Distance, Length() ); + return track_uneven::along( uneven_declared(), uneven_met( trPrev, end_point( *Segment, 0 ) ), uneven_met( trNext, end_point( *Segment, 1 ) ), Distance, Length() ); } -track_uneven::amounts TTrack::uneven_met( TTrack const *Other, glm::dvec3 const &Point ) const { +track_uneven::amounts TTrack::uneven_met( TTrack *Other, glm::dvec3 const &Point ) { if( Other == nullptr ) { return {}; @@ -182,7 +195,7 @@ track_uneven::amounts TTrack::uneven_met( TTrack const *Other, glm::dvec3 const return Other->eType == tt_Switch ? Other->switch_uneven_at( Point ) : track_uneven::least( uneven_declared(), Other->uneven_declared() ); } -TTrack::switch_stray const &TTrack::switch_uneven() const { +TTrack::switch_stray const &TTrack::switch_uneven() { if( m_switchstray.stamp != s_layoutstamp ) { settle_switch_stray(); @@ -190,26 +203,30 @@ TTrack::switch_stray const &TTrack::switch_uneven() const { return m_switchstray; } -void TTrack::settle_switch_stray() const { +void TTrack::settle_switch_stray() { auto const group { switch_group( *this ) }; auto const values { group_path_values( group ) }; - for( auto const *track : group ) { + for( auto *track : group ) { track->m_switchstray.paths = values; track->m_switchstray.stamp = s_layoutstamp; } - // at the ends of the paths, with the paths known in all of the group - for( auto const *track : group ) { - for( int path = 0; path < 2; ++path ) { - for( int end = 0; end < 2; ++end ) { - auto const point { track->path_segment( path )->end_point( end ) }; - auto value { track->switch_paths_at( point ) }; - auto const *other { track->path_neighbour( path, end ) }; - if( is_switch( other ) ) { - value = track_uneven::least( value, other->switch_paths_at( point ) ); - } - track->m_switchstray.ends[ path ][ end ] = value; + // with the paths known in all of the group + for( auto *track : group ) { + track->settle_switch_ends(); + } +} + +void TTrack::settle_switch_ends() { + + for( int path = 0; path < 2; ++path ) { + for( int end = 0; end < 2; ++end ) { + auto const point { end_point( *path_segment( path ), end ) }; + auto value { switch_paths_at( point ) }; + if( auto const *other { path_neighbour( path, end ) }; is_switch( other ) ) { + value = track_uneven::least( value, other->switch_paths_at( point ) ); } + m_switchstray.ends[ path ][ end ] = value; } } } @@ -217,13 +234,13 @@ void TTrack::settle_switch_stray() const { track_uneven::amounts TTrack::switch_paths_at( glm::dvec3 const &Point ) const { auto const &paths { m_switchstray.paths }; - return least_at_ends( *this, Point, track_uneven::least( paths[ 0 ], paths[ 1 ] ), [ & ]( int const Path, int ) { return paths[ Path ]; } ); + return least_at_ends( *this, Point, track_uneven::least( paths[ 0 ], paths[ 1 ] ), [ &paths ]( int const Path, int ) { return paths[ Path ]; } ); } -track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) const { +track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) { auto const &stray { switch_uneven() }; - return least_at_ends( *this, Point, track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ), [ & ]( int const Path, int const End ) { return stray.ends[ Path ][ End ]; } ); + return least_at_ends( *this, Point, track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ), [ &stray ]( int const Path, int const End ) { return stray.ends[ Path ][ End ]; } ); } double TTrack::joint_depth() const { @@ -234,7 +251,7 @@ double TTrack::joint_depth() const { return ( m_uneven.dip >= 0.f ? m_uneven.dip : track_uneven::dip_share * uneven_declared().up ); } -double TTrack::joint_dip( double const Distance ) const { +double TTrack::joint_dip( double const Distance ) { auto const phase { rail_phase( Distance ) }; if( phase < 0.0 ) { @@ -245,12 +262,12 @@ double TTrack::joint_dip( double const Distance ) const { return 0.0; } auto const depth { joint_depth() }; - auto const atend = [&]( TTrack const *Other ) { + auto const atend = [ this, depth ]( TTrack const *Other ) { return ( Other == nullptr || Other->fSoundDistance != fSoundDistance ? 0.0 : std::min( depth, Other->joint_depth() ) ); }; return 0.001 * track_uneven::dip( fromjoint ) * track_uneven::along( depth, atend( trPrev ), atend( trNext ), Distance, Length() ); } -double TTrack::rail_phase( double const Distance ) const { +double TTrack::rail_phase( double const Distance ) { if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { return -1.0; @@ -261,22 +278,22 @@ double TTrack::rail_phase( double const Distance ) const { return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); } -void TTrack::settle_rail_run() const { +void TTrack::settle_rail_run() { // the next track of the run past an end of a track, and which of its ends is joined there - auto const onward = []( TTrack const &Track, int const End, int &Joined ) -> TTrack const * { - auto const *other { End == 0 ? Track.trPrev : Track.trNext }; + auto const onward = []( TTrack const &Track, int const End, int &Joined ) -> TTrack * { + auto *other { End == 0 ? Track.trPrev : Track.trNext }; if( other == nullptr || other == &Track || other->joint_depth() <= 0.0 || other->fSoundDistance != Track.fSoundDistance ) { return nullptr; } Joined = ( End == 0 ? Track.iPrevDirection : Track.iNextDirection ) & 1; return other; }; // walks from this track to an end of the run - auto const walk = [&]( int &End ) { - auto const *track { this }; + auto const walk = [ this, &onward ]( int &End ) { + auto *track { this }; int joined { 0 }; for( int step = 0; step < 1000000; ++step ) { - auto const *next { onward( *track, End, joined ) }; + auto *next { onward( *track, End, joined ) }; if( next == nullptr || next == this ) { break; } @@ -285,15 +302,14 @@ void TTrack::settle_rail_run() const { } return track; }; auto const place = []( TTrack const &Track, int const End ) { - auto const point { Track.Segment->end_point( End ) }; + auto const point { end_point( *Track.Segment, End ) }; return std::make_pair( point.x, point.z ); }; // start at the end with the lesser coordinates, to get the same joints from any track of the run int end { 0 }; int otherend { 1 }; - auto const *track { walk( end ) }; - auto const *other { walk( otherend ) }; - if( place( *other, otherend ) < place( *track, end ) ) { + auto *track { walk( end ) }; + if( auto *other { walk( otherend ) }; place( *other, otherend ) < place( *track, end ) ) { track = other; end = otherend; } @@ -327,7 +343,8 @@ void TTrack::share_joints() { } } if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { - float roll1, roll2; + float roll1 { 0.f }; + float roll2 { 0.f }; Segment->GetRolls( roll1, roll2 ); if( roll1 != 0.f || roll2 != 0.f ) { iTrapezoid |= 1; diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 624e6db16..2c32ad660 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -51,18 +52,20 @@ struct wave { // the numbers of an entry like "uneven 14;35", negative where left out template -std::array read_fields( std::string const &Text ) { +std::array read_fields( std::string_view const Text ) { std::array result; result.fill( -1.f ); - std::size_t start { 0 }; - for( std::size_t index = 0; index < Count && start < Text.size(); ++index ) { - auto const end { std::min( Text.find( ';', start ), Text.size() ) }; - auto value { 0.f }; - if( end > start && std::from_chars( Text.data() + start, Text.data() + end, value ).ec == std::errc {} ) { - result[ index ] = std::clamp( value, 0.f, most ); + auto rest { Text }; + for( auto &field : result ) { + auto const end { std::min( rest.find( ';' ), rest.size() ) }; + if( auto value { 0.f }; end > 0 && std::from_chars( rest.data(), rest.data() + end, value ).ec == std::errc {} ) { + field = std::clamp( value, 0.f, most ); } - start = end + 1; + if( end == rest.size() ) { + break; + } + rest.remove_prefix( end + 1 ); } return result; } @@ -100,7 +103,7 @@ struct attribute { }; // a plain track: value;up;side;cross;dip. A switch: p1-p2;p3-p4;up of p1-p2;side of p1-p2;up of p3-p4;side of p3-p4 -inline attribute read_attribute( std::string const &Text, bool const Switch ) { +inline attribute read_attribute( std::string_view const Text, bool const Switch ) { auto const fields { read_fields<6>( Text ) }; attribute result; @@ -136,15 +139,15 @@ inline std::string write_attribute( attribute const &Values, bool const Switch ) // what a path comes to from the values: what's not given takes the first value, p3-p4 those of p1-p2. Cross level on plain tracks only inline amounts declared( attribute const &Values, bool const Plain, int const Path ) { + auto const given = []( float const Part, double const Otherwise ) { + return Part >= 0.f ? static_cast( Part ) : Otherwise; }; amounts result { - Values.up >= 0.f ? Values.up : Values.value, - Values.side >= 0.f ? Values.side : Values.value, - false == Plain ? 0.f : Values.cross >= 0.f ? Values.cross : Values.value }; + given( Values.up, Values.value ), + given( Values.side, Values.value ), + Plain ? given( Values.cross, Values.value ) : 0.0 }; if( Path == 1 ) { - auto const branch = [ &Values ]( float const Part, double const Straight ) -> double { - return Part >= 0.f ? Part : Values.branch >= 0.f ? Values.branch : Straight; }; - result.up = branch( Values.branchup, result.up ); - result.side = branch( Values.branchside, result.side ); + result.up = given( Values.branchup, given( Values.branch, result.up ) ); + result.side = given( Values.branchside, given( Values.branch, result.side ) ); } return result; } @@ -160,8 +163,9 @@ wave_set make_waves( double const Turn, std::array const & wave_set result {}; for( std::size_t i = 0; i < Count; ++i ) { auto const number { glm::two_pi() / Lengths[ i ] }; - auto const heading { Turn + golden * i }; - result[ i ] = { number * std::cos( heading ), number * std::sin( heading ), golden * ( i + 1 ) * ( 1.0 + 3.0 * Turn ), Shares[ i ] }; + auto const place { static_cast( i ) }; + auto const heading { Turn + golden * place }; + result[ i ] = { number * std::cos( heading ), number * std::sin( heading ), golden * ( place + 1.0 ) * ( 1.0 + 3.0 * Turn ), Shares[ i ] }; } return result; } @@ -246,7 +250,10 @@ inline glm::dvec3 across_of( glm::dvec3 const &Direction, glm::dvec3 const &Chor // the cant where two paths meet, theirs turned to the way of this one: a switch takes that of a plain track, alike ones meet halfway inline float joint_roll( float const Own, float const Their, bool const Switch, bool const Theirswitch ) { - return Switch == Theirswitch ? 0.5f * ( Own + Their ) : Switch ? Their : Own; + if( Switch == Theirswitch ) { + return 0.5f * ( Own + Their ); + } + return Switch ? Their : Own; } // each component the same way From 32fadd634d259595a961556fd5e6349895569dec Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH 03/32] refactor(tools): the grading tool in smaller functions As Sonar asks, and a regular expression with no backtracking. The results stay the same byte for byte on baltyk_skm1, baltyk_cargo, td and lk357. --- tools/track_uneven_grade.py | 348 +++++++++++++++++++++--------------- 1 file changed, 208 insertions(+), 140 deletions(-) diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index d643f94f8..1994559d0 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -43,7 +43,7 @@ STRAY_PER_MM, DIP_SHARE, DIP_REACH = 0.00145, 0.4, 3.0 GOLDEN = math.pi * (3.0 - math.sqrt(5.0)) UP_WAVES = [(2 * math.pi / length * math.cos(GOLDEN * i), 2 * math.pi / length * math.sin(GOLDEN * i), GOLDEN * (i + 1), share) for i, (length, share) in enumerate(zip((8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3), (.14, .19, .22, .22, .23, .23, .23, .23)))] TOKEN = re.compile(r"//[^\n]*|\S+") -NUMBER = re.compile(r"^[-+]?\d*\.?\d+([eE][-+]?\d+)?$") +NUMBER = re.compile(r"^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$") def limit(speed): @@ -79,38 +79,9 @@ class Scenery: def read(self, path, offset=(0.0, 0.0, 0.0)): path = path.resolve() - if path in self.seen: - return - self.seen.add(path) - text = path.read_bytes().decode("latin-1") - self.texts[path] = text - matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] - words = [m.group(0) for m in matches] - lower = [w.lower() for w in words] - origins = [offset] - i = 0 - while i < len(words): - word = lower[i] - if word == "include" and i + 1 < len(words): - end = i + 1 - while end < len(words) and lower[end] != "end": - end += 1 - self.include(words[i + 1], words[i + 2:end], origins[-1]) - i = end - elif word == "origin" and i + 3 < len(words) and all(NUMBER.match(w) for w in words[i + 1:i + 4]): - origins.append(tuple(o + float(w) for o, w in zip(origins[-1], words[i + 1:i + 4]))) - i += 3 - elif word == "endorigin" and len(origins) > 1: - origins.pop() - elif word == "rotate" and i + 3 < len(words) and any(w.strip("0.-") for w in words[i + 1:i + 4]): - self.notes["rotate"] += 1 - elif word == "node" and i + 4 < len(words): - kind = lower[i + 4] - if kind == "track": - i = self.track(path, matches, words, lower, i, origins[-1]) - elif kind == "model" and i + 9 < len(words) and GRASS.search(lower[i + 9]) and all(NUMBER.match(w) for w in words[i + 5:i + 8]): - self.plant(float(words[i + 5]) + origins[-1][0], float(words[i + 7]) + origins[-1][2]) - i += 1 + if path not in self.seen: + self.seen.add(path) + FileReader(self, path, offset).run() def include(self, name, params, origin): path = self.root / name.replace("\\", "/") @@ -118,21 +89,90 @@ class Scenery: numbers = [float(w) for w in params if NUMBER.match(w)] if GRASS.search(name.lower()) and len(numbers) >= 3: self.plant(numbers[0] + origin[0], numbers[2] + origin[2]) - return - if not path.is_file(): + elif not path.is_file(): self.notes["brak pliku " + name] += 1 - return - if params: + elif not params: + self.read(path, origin) + elif path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): # the same file can come many times with other parameters: its tracks can't be told apart in one copy - if path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): - self.notes["pliki z parametrami, tory pominiete"] += 1 - return - self.read(path, origin) + self.notes["pliki z parametrami, tory pominiete"] += 1 def plant(self, x, z): self.plants[(round(x / 10), round(z / 10))].append((x, z)) - def track(self, path, matches, words, lower, i, origin): + +class FileReader: + """Goes through the words of one file. Each handler takes the place of its word and gives the place of the last word it used.""" + + def __init__(self, scenery, path, offset): + self.scenery, self.path = scenery, path + text = path.read_bytes().decode("latin-1") + scenery.texts[path] = text + self.matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] + self.words = [m.group(0) for m in self.matches] + self.lower = [w.lower() for w in self.words] + self.origins = [offset] + self.handlers = {"include": self.include, "origin": self.origin, "endorigin": self.endorigin, "rotate": self.rotate, "node": self.node} + + def run(self): + i = 0 + while i < len(self.words): + handler = self.handlers.get(self.lower[i]) + i = (handler(i) if handler else i) + 1 + + def include(self, i): + if i + 1 >= len(self.words): + return i + end = i + 1 + while end < len(self.words) and self.lower[end] != "end": + end += 1 + self.scenery.include(self.words[i + 1], self.words[i + 2:end], self.origins[-1]) + return end + + def origin(self, i): + values = self.words[i + 1:i + 4] + if len(values) < 3 or not all(NUMBER.match(w) for w in values): + return i + self.origins.append(tuple(o + float(w) for o, w in zip(self.origins[-1], values))) + return i + 3 + + def endorigin(self, i): + if len(self.origins) > 1: + self.origins.pop() + return i + + def rotate(self, i): + if i + 3 < len(self.words) and any(w.strip("0.-") for w in self.words[i + 1:i + 4]): + self.scenery.notes["rotate"] += 1 + return i + + def node(self, i): + if i + 4 >= len(self.words): + return i + kind = self.lower[i + 4] + if kind == "track": + return self.track(i) + if kind == "model" and i + 9 < len(self.words) and GRASS.search(self.lower[i + 9]) and all(NUMBER.match(w) for w in self.words[i + 5:i + 8]): + self.scenery.plant(float(self.words[i + 5]) + self.origins[-1][0], float(self.words[i + 7]) + self.origins[-1][2]) + return i + + def paths_from(self, cursor, count): + paths = [] + origin = self.origins[-1] + for _ in range(count): + values = self.words[cursor:cursor + 15] + if len(values) < 15 or not all(NUMBER.match(v) for v in values): + return None + n = [float(v) for v in values] + part = {"p1": [n[j] + origin[j] for j in range(3)], "c1": n[4:7], "c2": n[7:10], "p2": [n[10 + j] + origin[j] for j in range(3)], "radius": abs(n[14])} + part["line"] = line_of(part) + part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:])) + paths.append(part) + cursor += 15 + return paths + + def track(self, i): + words, lower = self.words, self.lower at = i + 4 end = at while end < len(words) and lower[end] != "endtrack": @@ -142,33 +182,30 @@ class Scenery: kind = lower[at + 1] if kind not in ("normal", "switch", "road"): return end - cursor = at + 10 - rail = ballast = None - if lower[at + 9] == "vis": - rail, ballast = lower[cursor], lower[cursor + 2] - cursor += 6 - paths = [] - for _ in range(2 if kind == "switch" else 1): - values = words[cursor:cursor + 15] - if len(values) < 15 or not all(NUMBER.match(v) for v in values): - self.notes["tory z parametrami, pominiete"] += 1 - return end - n = [float(v) for v in values] - part = dict(p1=[n[j] + origin[j] for j in range(3)], c1=n[4:7], c2=n[7:10], p2=[n[10 + j] + origin[j] for j in range(3)], radius=abs(n[14])) - part["line"] = line_of(part) - part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:])) - paths.append(part) - cursor += 15 - if kind == "road": - self.roads.append(paths[0]) - return end - block = lower[at:end] - taken = any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip")) - velocity = float(words[at + block.index("velocity") + 1]) if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]) else 0.0 - sound = float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0 - self.tracks.append(dict(name=words[i + 3], kind=kind, rail=rail, ballast=ballast, sound=sound, paths=paths, speed=velocity if velocity > 0 else None, source="wpis" if velocity > 0 else None, file=path, offset=matches[end].start(), taken=taken)) + visible = lower[at + 9] == "vis" + paths = self.paths_from(at + (16 if visible else 10), 2 if kind == "switch" else 1) + if paths is None: + self.scenery.notes["tory z parametrami, pominiete"] += 1 + elif kind == "road": + self.scenery.roads.append(paths[0]) + else: + self.add_track(i, at, end, kind, visible, paths) return end + def add_track(self, i, at, end, kind, visible, paths): + words = self.words + block = self.lower[at:end] + velocity = 0.0 + if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]): + velocity = float(words[at + block.index("velocity") + 1]) + self.scenery.tracks.append({ + "name": words[i + 3], "kind": kind, + "rail": self.lower[at + 10] if visible else None, "ballast": self.lower[at + 12] if visible else None, + "sound": float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0, + "paths": paths, "speed": velocity if velocity > 0 else None, "source": "wpis" if velocity > 0 else None, + "file": self.path, "offset": self.matches[end].start(), + "taken": any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip"))}) + def bezier(part, t): a = [part["p1"][i] + part["c1"][i] for i in range(3)] @@ -220,50 +257,67 @@ class Joints: return found +def plain_neighbours(t, joints): + return [other for key in ("p1", "p2") for other, _, _ in joints.at(t["paths"][0][key]) if other is not t and other["kind"] == "normal"] + + +def plain_past_switches(t, point, joints): + # the plain tracks reached from a point of a track along the straight paths of switches + points, seen, found = [point], set(), [] + for _ in range(6): + onward = [] + for reached in points: + for other, part, side in joints.at(reached): + if other is t or id(part) in seen: + continue + seen.add(id(part)) + if other["kind"] != "switch": + found.append(other) + elif other["paths"].index(part) != other["bent"]: + onward.append(part["p2" if side == "p1" else "p1"]) + points = onward + return found + + +def short_pieces_past_switches(t, joints): + return [o for key in ("p1", "p2") for o in plain_past_switches(t, t["paths"][0][key], joints) if o["paths"][0]["length"] < PIECE] + + +def spread_speed(tracks, joints, reached): + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for other in reached(t, joints): + if other["speed"] is None: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + + def settle_speeds(tracks, joints): # from the nearest track on the stretch between switches, a short piece also along the straight path of a switch - heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) - while heads: - t = heads.popleft() - for key in ("p1", "p2"): - for other, _, _ in joints.at(t["paths"][0][key]): - if other is not t and other["kind"] == "normal" and other["speed"] is None: - other["speed"], other["source"] = t["speed"], "sasiad" - heads.append(other) - heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) - while heads: - t = heads.popleft() - for key in ("p1", "p2"): - points, seen = [t["paths"][0][key]], set() - for _ in range(6): - onward = [] - for point in points: - for other, part, side in joints.at(point): - if other is t or id(part) in seen: - continue - seen.add(id(part)) - if other["kind"] == "switch": - if other["paths"].index(part) != other["bent"]: - onward.append(part["p2" if side == "p1" else "p1"]) - elif other["speed"] is None and other["paths"][0]["length"] < PIECE: - other["speed"], other["source"] = t["speed"], "sasiad" - heads.append(other) - points = onward + spread_speed(tracks, joints, plain_neighbours) + spread_speed(tracks, joints, short_pieces_past_switches) for t in tracks: if t["speed"] is None: t["speed"], t["source"] = NO_SPEED, "brak" +def plain_ends(tracks): + for t in tracks: + if t["kind"] == "normal": + for part in t["paths"]: + yield part["p1"] + yield part["p2"] + + def distances(tracks, joints, station): best, heap = {}, [] - for t in tracks: - for part in t["paths"]: - for key in ("p1", "p2"): - gap = math.dist((part[key][0], part[key][2]), station) - node = (round(part[key][0], 1), round(part[key][2], 1)) - if gap < 300 and t["kind"] == "normal" and gap < best.get(node, math.inf): - best[node] = gap - heapq.heappush(heap, (gap, node, part[key])) + for point in plain_ends(tracks): + gap = math.dist((point[0], point[2]), station) + node = (round(point[0], 1), round(point[2], 1)) + if gap < 300 and gap < best.get(node, math.inf): + best[node] = gap + heapq.heappush(heap, (gap, node, point)) while heap: distance, node, point = heapq.heappop(heap) if distance > best.get(node, math.inf): @@ -293,30 +347,34 @@ def grass_on(part, plants): return len(near) * 100.0 / max(part["length"], 5.0) +def cells_under(a, b): + for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): + for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): + yield x, z + + def road_cells(roads): cells = collections.defaultdict(list) for road in roads: for a, b in zip(road["line"], road["line"][1:]): - for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): - for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): - cells[(x, z)].append((a, b)) + for cell in cells_under(a, b): + cells[cell].append((a, b)) return cells +def cross_in_plan(a, b, c, d): + rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2] + k = rx * sz - rz * sx + if abs(k) < 1e-9: + return False + t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k + u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k + return 0 <= t <= 1 and 0 <= u <= 1 + + def hits_road(part, cells): - for a, b in zip(part["line"], part["line"][1:]): - for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): - for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): - for c, d in cells.get((x, z), ()): - rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2] - k = rx * sz - rz * sx - if abs(k) < 1e-9: - continue - t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k - u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k - if 0 <= t <= 1 and 0 <= u <= 1: - return True - return False + line = part["line"] + return any(cross_in_plan(a, b, c, d) for a, b in zip(line, line[1:]) for cell in cells_under(a, b) for c, d in cells.get(cell, ())) def heights_per_mm(t): @@ -423,17 +481,19 @@ def fit_under_pantograph(normal): return low +def joint_curve_changes(t, part, key, joints): + own = end_curve(part, key) + for other, theirs, side in joints.at(part[key]): + if other is not t and other["kind"] == "normal": + their = end_curve(theirs, side) + if abs(own - (-their if side == key else their)) > CURVE_STEP: + return True + return False + + def curve_changes(t, joints): part = t["paths"][0] - if abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP: - return True - for key in ("p1", "p2"): - for other, theirs, side in joints.at(part[key]): - if other is not t and other["kind"] == "normal": - their = end_curve(theirs, side) - if abs(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: - return True - return False + return abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP or any(joint_curve_changes(t, part, key, joints) for key in ("p1", "p2")) def move_near_limit(normal, joints, share): @@ -458,24 +518,32 @@ def move_near_limit(normal, joints, share): return chosen +def value_from_plain(t, index, part, joints, product): + found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] + if not found: + return None + speed = max(o["speed"] for o in found) + if index == t["bent"]: + speed = min(speed, DIVERGING) + return min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + + +def value_from_switches(t, part, joints): + found = [] + for key in ("p1", "p2"): + for other, theirs, _ in joints.at(part[key]): + if other["kind"] == "switch" and other is not t: + found += [other["values"][i] for i, q in enumerate(other["paths"]) if q is theirs and other["values"][i] is not None] + return max(found) if found else None + + def grade_switches(switches, joints, product): # each path by the plain tracks it leads to, the worst of them; those only among switches from their neighbours for t in switches: - t["values"] = [None] * len(t["paths"]) - for index, part in enumerate(t["paths"]): - found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] - if found: - speed = max(o["speed"] for o in found) - if index == t["bent"]: - speed = min(speed, DIVERGING) - t["values"][index] = min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + t["values"] = [value_from_plain(t, index, part, joints, product) for index, part in enumerate(t["paths"])] for _ in range(5): for t in switches: - for index, part in enumerate(t["paths"]): - if t["values"][index] is None: - found = [o["values"][i] for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "switch" and o is not t for i, q in enumerate(o["paths"]) if q is theirs and o["values"][i] is not None] - if found: - t["values"][index] = max(found) + t["values"] = [value_from_switches(t, part, joints) if value is None else value for value, part in zip(t["values"], t["paths"])] for t in switches: t["values"] = [min(SWITCH_MOST, product / NO_SPEED) if v is None else v for v in t["values"]] From dbc173ebeaf0d25c839e0e3df8bbe8dce76ef775 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH 04/32] docs(track): what of the cab shake goes off on an uneven track A table of all the sources of the shake, and what happens to each of them. --- track_uneven_doc.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/track_uneven_doc.md b/track_uneven_doc.md index c7446e34d..1c3163c50 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -69,7 +69,19 @@ The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the bo Models from includes (e.g. switch drives and timbers), the terrain and the catenary don't follow it, so at large values a rail shifts away from such a model by a few centimetres. -On a track with unevenness the track itself shakes the cab. The sway calculated from the lateral acceleration, that is from the curve radius, and the random jolts at higher speed are off. On tracks with no unevenness they work as before. +On a track with unevenness the track itself shakes the cab, in place of two of the sources of shake that were there before. It goes by the track under the vehicle, and a track with only dips at its joints counts too. On tracks with no unevenness everything works as before. + +| source of the cab shake | on a track with unevenness | +|--------------------------------------------------------------------|----------------------------| +| side sway from the lateral acceleration: curve radius and cant, jolts sideways on a damaged switch | off | +| random jolts over 25 km/h | off | +| the track itself, from the bend under the axles | added | +| vertical jolts at the joints of a damaged track | stays | +| fore and aft, when accelerating and braking | stays | +| diesel engine vibration | stays | +| hunting (`huntingspring`), set by the vehicle | stays | + +This is about the cab only. The physics, derailment included, and the noise of the wheels still use the lateral acceleration. A pantograph rises at the rate set by the pressure, about 0.14 m/s, and a gap to the wire over 1 cm cuts the voltage and trips the main breaker. Unevenness adds to this at high speed. On the tracks of Bałtyk a simulation gives a gap of 0.6 cm when the vertical value times the speed in km/h is 925, for example 8 mm at 120 km/h, 13 mm at 70 km/h and 23 mm at 40 km/h. The values in the Id-14 table are higher than that. The wire alone can do it too: at `tor0290` on Bałtyk it rises by about 1%, so a pantograph keeps up to about 46 km/h there, and above that the breaker trips on a straight track as well. From d995674bee8b45465fd98489a1559bf4d515765a Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sun, 1 Nov 2026 09:12:36 +0100 Subject: [PATCH 05/32] =?UTF-8?q?fix(physics):=20automatic=20couplers=20do?= =?UTF-8?q?n't=20catch=20above=20the=20safe=20collision=E2=80=A6=20(#217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds fix(cab): the line contactors lamp goes out with the pressure switch (eu07/maszyna#470) The lamp of the open line contactors, which tells the start can be made, went out at 1 bar in the brake cylinders, while the pressure switch cuts the start off only at 2 bar (or below 3.6 bar in the main pipe), so with the brakes partly applied the start could be made with the lamp out. With a pressure switch fitted the lamp now follows it; without one it goes out at 1 bar as before. The same holds for the lamp of the second unit, which used to keep its last state between 1 bar and the cut-off. The condition is TMoverParameters::LineContactorsLampLit(). Tests: tests/unit/test_line_contactors.cpp. refactor: Sonar findings of PR 217 - horn_supply::pneumatic shadowed the pneumatic enumerator of the sound flags (cpp:S1117), it's horn_supply::air; the supplies read with using enum (S6177), - CouplerForce(): the automatic coupling refused or caught without another level of nested ifs (S134), the same outcome: too fast, the coupling is locked; caught, coupled and locked; otherwise left to try again, - CompressorCheck(): the power the compressor doesn't draw yet noted without a TODO (S1135), - tests: the previous crash damage setting in an in-class initializer (S3230), the loops which stop early with break rather than in their conditions (S886), the stand-ins of test::pair in a std::vector rather than an array (S5945), units in the comments of the example without /* */ (S1103). test: the cases the changes of the branch left without a test - Cntrl.: BDelay1 to BDelay4 reach the brake delays (now through std::format), the default start types of the ground and converter overload relays of a multiple unit and of other vehicles (out of the nested conditionals), - the compressor: fed from the low voltage it waits from the low voltage coming on; the pressure switch stops it above the cut-out pressure, without one the excess air is vented (the vented fraction rewritten), - the horns: a vehicle without any air sounds as before, the threshold pressure, an electric horn on 110 V, - the positions of a cab control below zero, and a control without position sounds. refactor(cab): the kind of the train heating switch as a flag Read once with the vehicle definition, rather than compared as text in each of the switch commands. feat(cab): train heating switch springing back (MaSzyna-EU07/maszyna#85) TrainHeating=[toggle]|impulse in the Switches: section of the vehicle definition makes the train heating switch an unstable one: it goes up to switch the heating on, down to switch it off, and back to the middle once let go, instead of staying where it was put. The position of such a switch isn't set from the heating state when the cab is entered. Tests: tests/unit/test_switches.cpp. feat(sound): mirror sounds (MaSzyna-EU07/maszyna#84, eu07/maszyna#322) The sound definitions of a vehicle (.mmd) take mirroropen: and mirrorclose:, played as a mirror sets off unfolding or folding, and the locations section mirrors: ... end, the offsets along the vehicle of the mirror pairs, as for the doors (negative ahead of the centre), each pair at the sides of the vehicle. As the animation, only the mirrors at the occupied cab sound. The start of a move is mirror_moves::motion in vehicle/mirrors.h. Tests: tests/unit/test_mirrors.cpp. feat(cab): joint oil and fuel pump switch (MaSzyna-EU07/maszyna#81) A cab control oilfuelpump_sw: switches both the oil and the fuel pump at once, with the new command oilfuelpumptoggle (no key by default). As a two-state switch it turns both pumps off when both are on and both on otherwise; as an impulse switch it's held to start them, like the separate pump switches. Its position follows the pumps when the cab is entered. refactor(editor): text fields filled without strncpy (MaSzyna-EU07/maszyna#112) The last three strncpy calls Sonar flags (cpp:S5816), in the road and track editors, copy the text into the ImGui field buffers with std::string::copy and terminate it right after; the text gets cut to the buffer as before. fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds - automatic couplers don't catch above the safe collision speed (#86): with crash damage on they couple at a closing speed of 15 km/h at most, a faster hit locks the coupling until the vehicles part and the collision runs its course. On the way: CollisionVelocity() read a head-on 10 + 10 km/h as 14.1 km/h (quaternions turn by half the angle), it's now taken from the touching ends; CollisionDetect() took the other vehicle's speed the wrong way round for vehicles facing each other. Head-on hits now derail sooner, at the intended limit, - a single horn lever can't sound both tones at once (eu07/maszyna#392), - a skid ends once the vehicle stops on locked wheels (eu07/maszyna#479), - electronic destination signs are blanked at the end of the timetable (eu07/maszyna#446), - horns need the air or the voltage to sound (eu07/maszyna#290, #76): HornType=[Pneumatic]|Electric and Horn3Type in Cntrl.; a pneumatic horn needs 0.2 MPa in the main reservoir (or the main pipe without one), an electric one the low voltage; the lever stays where it was put, - compressor start delay (#67): CompressorStartDelay [s] in Brake., counted from the compressor getting its power, after the engine starts say; none by default, - the channel and volume buttons of a radio without power do nothing (eu07/maszyna#429); the power check of the radio is TMoverParameters::RadioPowered(), - a lever moved smoothly sounds the positions it passes (#83). Sonar findings in the functions changed: nesting, nested conditionals, commented-out code, std::format, references to const. Unit tests: test::pair for two vehicles running into each other, test_couplers, test_horn, test_wheelslip, test_compressor, test_radio, test_gauge_positions. * fix: review findings of PR 217 - the pressures of the mover are in bar (MinCP=7.0 in the vehicle definitions), so the 0.2 the pneumatic horns needed was 0.2 bar; it's 2 bar now, and the tests take their pressures in bar too, - the lamps of the ground relay and of the motor overload relays follow the pressure switch as the line contactors one does, TMoverParameters::MainCircuitLampsEnabled(), so the lamps of one cab go out together, - the joint oil and fuel pump switch leaves out a pump started automatically, which ignores the switching: with one, it couldn't turn the other off, and showed off with both running (OilFuelPumpsEnabled()), - the kind of the train heating switch comes from the occupied vehicle on entering the cab too, as in the switch commands; in a multiple unit these are different vehicles, - CouplerForce() put the force taken back by the other vehicle on its far coupler when the vehicles faced each other (Couplers[1 - End] rather than the coupler it's coupled with), - the velocity difference along the coupler axis computed once, CouplerVelocityDifference(), for CouplerForce() and CollisionVelocity(), - the attachcoupler flag added without the SetFlag() call with 0, the index of the mirror moves noted as by side. Tests: the coupler force of vehicles facing each other, the joint switch with an automatic pump, the lamps of the main circuit, the horn and compressor pressures in bar. * fix: coupler force of vehicles facing each other taken back along the other vehicle's axis CForce runs along the axis of its own vehicle, so the force the other vehicle takes back on its coupler of the pair is turned around once more when the vehicles face each other (-CF * DirPatch()), as that vehicle computes it itself. Tests: the coupler forces of the pair facing each other and facing the same way. --- CMakeLists.txt | 1 + McZapkie/MOVER.h | 50 +++ McZapkie/Mover.cpp | 199 +++++++----- application/editormoderoad.cpp | 4 +- application/editormodetrack.cpp | 7 +- input/command.cpp | 2 + input/command.h | 1 + input/drivermouseinput.cpp | 3 + tests/unit/README.md | 6 +- tests/unit/test_cntrl.cpp | 39 +++ tests/unit/test_compressor.cpp | 115 +++++++ tests/unit/test_couplers.cpp | 458 ++++++++++++++++++++++++++++ tests/unit/test_gauge_positions.cpp | 58 ++++ tests/unit/test_horn.cpp | 121 ++++++++ tests/unit/test_line_contactors.cpp | 59 ++++ tests/unit/test_mirrors.cpp | 32 ++ tests/unit/test_pumps.cpp | 52 ++++ tests/unit/test_radio.cpp | 28 ++ tests/unit/test_switches.cpp | 16 + tests/unit/test_wheelslip.cpp | 62 ++++ tests/unit/vehicle.cpp | 104 ++++++- tests/unit/vehicle.h | 51 ++++ utilities/translation.cpp | 1 + vehicle/Driver.cpp | 27 +- vehicle/DynObj.cpp | 91 +++++- vehicle/DynObj.h | 16 +- vehicle/Gauge.cpp | 42 ++- vehicle/Train.cpp | 185 ++++++----- vehicle/Train.h | 7 +- vehicle/gaugepositions.h | 35 +++ vehicle/horn.cpp | 24 ++ vehicle/horn.h | 24 ++ vehicle/mirrors.h | 44 +++ vr/openvr_imp.cpp | 3 + 34 files changed, 1765 insertions(+), 202 deletions(-) create mode 100644 tests/unit/test_cntrl.cpp create mode 100644 tests/unit/test_compressor.cpp create mode 100644 tests/unit/test_couplers.cpp create mode 100644 tests/unit/test_gauge_positions.cpp create mode 100644 tests/unit/test_horn.cpp create mode 100644 tests/unit/test_line_contactors.cpp create mode 100644 tests/unit/test_mirrors.cpp create mode 100644 tests/unit/test_pumps.cpp create mode 100644 tests/unit/test_radio.cpp create mode 100644 tests/unit/test_switches.cpp create mode 100644 tests/unit/test_wheelslip.cpp create mode 100644 vehicle/gaugepositions.h create mode 100644 vehicle/horn.cpp create mode 100644 vehicle/horn.h create mode 100644 vehicle/mirrors.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 477f584e9..dfe58b66d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,7 @@ set(CORE_SOURCES "utilities/utilities.cpp" "scripting/ladderlogic.cpp" "vehicle/handover.cpp" +"vehicle/horn.cpp" ) add_library(eu07_core STATIC ${CORE_SOURCES}) target_link_libraries(eu07_core PUBLIC eu07_settings) diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index 29b3e75cd..bb36694bb 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -676,6 +676,13 @@ struct TTractionParam }; /*powyzsze parametry zwiazane sa z torem po ktorym aktualnie pojazd jedzie*/ +// what a horn needs to sound: air from the main reservoir, or the low voltage +enum class horn_supply +{ + air, + electric +}; + /// High-level brake-system family. enum class TBrakeSystem { @@ -1710,6 +1717,10 @@ class TMoverParameters float DistanceCounterDoublePressPeriod = {1.f}; // czas w jakim nalezy podwojnie wcisnac przycisk, aby rozpoczac pomiar odleglosci TSecuritySystem SecuritySystem; int EmergencyBrakeWarningSignal{0}; // combined with basic WarningSignal when manual emergency brake is active + horn_supply HornSupply{horn_supply::air}; // of the low and high tone horns (WarningSignal 1 and 2) + horn_supply Horn3Supply{horn_supply::air}; // of the third horn (WarningSignal 4) + // main reservoir pressure [bar, as MinCP and MaxCP] below which a pneumatic horn stays silent + static constexpr double HornMinPressure{2.0}; TUniversalCtrlTable UniCtrlList; /*lista pozycji uniwersalnego nastawnika*/ int UniCtrlListSize = 0; /*wielkosc listy pozycji uniwersalnego nastawnika*/ bool UniCtrlIntegratedBrakePNCtrl = false; /*zintegrowany nastawnik JH obsluguje hamulec PN*/ @@ -2001,6 +2012,8 @@ class TMoverParameters bool CompressorAllow = false; /*! zezwolenie na uruchomienie sprezarki NBMX*/ bool CompressorAllowLocal{true}; // local device state override (most units don't have this fitted so it's set to true not to intefere) bool CompressorGovernorLock{false}; // indicates whether compressor pressure switch was activated due to reaching cut-out pressure + double CompressorStartDelay{0.0}; // time [s] the compressor waits after getting its power before it can start, after the engine starts say + double CompressorPowerTime{0.0}; // time [s] since the compressor got its power bool CompressorTankValve{false}; // indicates excessive pressure is vented from compressor tank directly and instantly start_t CompressorStart{start_t::manual}; // whether the compressor is started manually, or another way start_t PantographCompressorStart{start_t::manual}; @@ -2228,6 +2241,7 @@ class TMoverParameters // TODO: move these switch types where they belong, cabin definition std::string PantSwitchType; std::string ConvSwitchType; + bool HeatingSwitchImpulse{false}; // the train heating switch springs back to its middle position std::string StLinSwitchType; bool Heating = false; // ogrzewanie 'Winger 020304 @@ -2428,6 +2442,42 @@ class TMoverParameters double BrakeForce(const TTrackParam &Track); double CouplerForce(int const End, double dt); void CollisionDetect(int const End, double const dt); + // closing speed of a collision [km/h] above which the vehicles involved may derail, adjusted for their mass ratio + static constexpr double SafeCollisionVelocity{15.0}; + // closing speed [km/h] of the End of this vehicle and the OtherEnd of the other vehicle it touches, along the coupler + // axis: the sum of the speeds in a head-on collision, the difference in a rear-end one, negative when they part + double CollisionVelocity(int const End, TMoverParameters const &Other, int const OtherEnd) const; + // velocity [m/s] of this vehicle relative to the other vehicle it touches with its End and the OtherEnd of the + // other one, along this vehicle's axis + double CouplerVelocityDifference(int const End, TMoverParameters const &Other, int const OtherEnd) const; + // whether the automatic couplers at the End of this vehicle and the OtherEnd of the other one can catch each other on + // contact, rather than the vehicles colliding (with crash damage enabled, only at a closing speed which doesn't derail them) + bool AutomaticCouplingSafe(int const End, TMoverParameters const &Other, int const OtherEnd) const; + // the horns of the Signal (WarningSignal bits) which have the air or the voltage they need to sound + int SoundingHorns(int const Signal) const; + // whether the signal lamps of the main circuit (line contactors, ground relay, motor overload relays) can light: + // the pressure switch, when fitted, cuts the start off and puts them out, otherwise they go out once the brakes are applied + bool MainCircuitLampsEnabled() const + { + return false == ControlPressureSwitch && (HasControlPressureSwitch || BrakePress < 1.0); + } + // whether the lamp of the open line contactors is lit, telling the start can be made + bool LineContactorsLampLit() const + { + return false == StLinFlag && MainCircuitLampsEnabled(); + } + // whether the pumps a joint oil and fuel pump switch works, those not started automatically, are all switched on + bool OilFuelPumpsEnabled() const + { + auto const fuelmanual{FuelPump.start_type != start_t::automatic}; + auto const oilmanual{OilPump.start_type != start_t::automatic}; + return (fuelmanual || oilmanual) && (false == fuelmanual || FuelPump.is_enabled) && (false == oilmanual || OilPump.is_enabled); + } + // whether the radio is switched on and has the low voltage to work + bool RadioPowered() const + { + return Radio && (Power24vIsAvailable || Power110vIsAvailable); + } /*obrot kol uwzgledniajacy poslizg*/ double ComputeRotatingWheel(double WForce, double dt, double n) const; diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 3b5b62153..cb17e30ad 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -16,6 +16,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/parser.h" #include "simulation/simulation.h" + +#include //--------------------------------------------------------------------------- // Ra: tu należy przenosić funcje z mover.pas, które nie są z niego wywoływane. @@ -1176,6 +1178,56 @@ double TMoverParameters::EngineMaxRPM() const std::numeric_limits::max(); // shouldn't ever get here but, eh } +// ************************************************************************************************* +// prędkość zbliżania się stykających się końców pojazdów przy zderzeniu, w km/h +// ************************************************************************************************* +double TMoverParameters::CollisionVelocity(int const End, TMoverParameters const &Other, int const OtherEnd) const +{ + auto const velocitydifference{CouplerVelocityDifference(End, Other, OtherEnd)}; + // the front end closes in on the other vehicle when this one moves forward faster, the rear end when it's slower + return (End == end::front ? velocitydifference : -velocitydifference) * 3.6; // m/s -> km/h +} + +double TMoverParameters::CouplerVelocityDifference(int const End, TMoverParameters const &Other, int const OtherEnd) const +{ + // V runs along the vehicle's own axis, towards its front; the other vehicle's velocity is turned around when the + // vehicles face each other (touch with the same ends) + return V - Other.V * DirPatch(End, OtherEnd); +} + +bool TMoverParameters::AutomaticCouplingSafe(int const End, TMoverParameters const &Other, int const OtherEnd) const +{ + return false == Global.crash_damage || CollisionVelocity(End, Other, OtherEnd) <= SafeCollisionVelocity; +} + +int TMoverParameters::SoundingHorns(int const Signal) const +{ + auto const supplied{[this](horn_supply const Supply) { + if (Supply == horn_supply::electric) + { + return Power24vIsAvailable || Power110vIsAvailable; + } + if (VeselVolume > 0.0) + { + return Compressor >= HornMinPressure; + } + // a vehicle without a main reservoir of its own takes the air from the main pipe, a vehicle without either + // (a road vehicle, say) has no air to check + auto const mainpipe{((Couplers[end::front].AllowedFlag | Couplers[end::rear].AllowedFlag) & coupling::mainhose) != 0}; + return false == mainpipe || ScndPipePress >= HornMinPressure; + }}; + auto horns{0}; + if (supplied(HornSupply)) + { + horns |= Signal & (1 | 2); + } + if (supplied(Horn3Supply)) + { + horns |= Signal & 4; + } + return horns; +} + // ************************************************************************************************* // Q: 20160716 // Wykrywanie kolizji @@ -1192,8 +1244,11 @@ void TMoverParameters::CollisionDetect(int const End, double const dt) auto const otherend{Neighbours[End].vehicle_end}; auto const &othercoupler{othervehicle->Couplers[otherend]}; + // the velocities along this vehicle's axis: the other vehicle's one is turned around when the vehicles face each + // other (touch with the same ends), as in CouplerForce() + auto const otherdirection{DirPatch(End, otherend)}; auto velocity{V}; - auto othervehiclevelocity{othervehicle->V}; + auto othervehiclevelocity{othervehicle->V * otherdirection}; // calculate collision force and new velocities for involved vehicles auto const VirtualCoupling{(thiscoupler.CouplingFlag == coupling::faux)}; auto CCF{0.0}; @@ -1224,28 +1279,25 @@ void TMoverParameters::CollisionDetect(int const End, double const dt) damage_coupler(End); } - if (thiscoupler.CouplingFlag == coupling::faux || true == TestFlag(othervehicle->DamageFlag, dtrain_out)) - { // HACK: limit excessive speed derailment checks to vehicles which aren't part of the same consist - auto const safevelocitylimit{15.0}; - auto const velocitydifference{glm::length(glm::angleAxis(Rot.Rz, glm::dvec3{0, 1, 0}) * V - glm::angleAxis(othervehicle->Rot.Rz, glm::dvec3{0, 1, 0}) * othervehicle->V) * - 3.6}; // m/s -> km/h + // HACK: limit excessive speed derailment checks to vehicles which aren't part of the same consist + auto const outsideconsist{thiscoupler.CouplingFlag == coupling::faux || true == TestFlag(othervehicle->DamageFlag, dtrain_out)}; + auto const velocitydifference{outsideconsist ? CollisionVelocity(End, *othervehicle, otherend) : 0.0}; - if (velocitydifference > safevelocitylimit) + if (velocitydifference > SafeCollisionVelocity) + { + // HACK: crude estimation for potential derail, will take place with velocity difference > 15 km/h adjusted for vehicle mass ratio + if (false == TestFlag(DamageFlag, dtrain_out) || false == TestFlag(othervehicle->DamageFlag, dtrain_out)) { - // HACK: crude estimation for potential derail, will take place with velocity difference > 15 km/h adjusted for vehicle mass ratio - if (false == TestFlag(DamageFlag, dtrain_out) || false == TestFlag(othervehicle->DamageFlag, dtrain_out)) - { - WriteLog("Bad driving: " + Name + " and " + othervehicle->Name + " collided with velocity " + to_string(velocitydifference, 0) + " km/h"); - } + WriteLog("Bad driving: " + Name + " and " + othervehicle->Name + " collided with velocity " + to_string(velocitydifference, 0) + " km/h"); + } - if (velocitydifference > safevelocitylimit * (TotalMass / othervehicle->TotalMass)) - { - Derail(COLLISION); - } - if (velocitydifference > safevelocitylimit * (othervehicle->TotalMass / TotalMass)) - { - othervehicle->Derail(COLLISION); - } + if (velocitydifference > SafeCollisionVelocity * (TotalMass / othervehicle->TotalMass)) + { + Derail(COLLISION); + } + if (velocitydifference > SafeCollisionVelocity * (othervehicle->TotalMass / TotalMass)) + { + othervehicle->Derail(COLLISION); } } } @@ -1254,21 +1306,15 @@ void TMoverParameters::CollisionDetect(int const End, double const dt) if (false == TestFlag(DamageFlag, dtrain_out)) { auto const accelerationchange{(velocity - V) / dt}; - // if( accelerationchange / AccS < 1.0 ) { - // HACK: prevent excessive vehicle pinball cases AccS += accelerationchange; - // AccS = std::clamp( AccS, -2.0, 2.0 ); V = velocity; - // } } if (false == TestFlag(othervehicle->DamageFlag, dtrain_out)) { - auto const othervehicleaccelerationchange{(othervehiclevelocity - othervehicle->V) / dt}; - // if( othervehicleaccelerationchange / othervehicle->AccS < 1.0 ) { - // HACK: prevent excessive vehicle pinball cases + auto const othervehiclenewvelocity{othervehiclevelocity * otherdirection}; // back along the other vehicle's axis + auto const othervehicleaccelerationchange{(othervehiclenewvelocity - othervehicle->V) / dt}; othervehicle->AccS += othervehicleaccelerationchange; - othervehicle->V = othervehiclevelocity; - // } + othervehicle->V = othervehiclenewvelocity; } } @@ -4332,7 +4378,10 @@ void TMoverParameters::CompressorCheck(double dt) } } - auto const compressorpower{(CompressorPower == 0 ? Mains : CompressorPower == 3 ? Mains : Power110vIsAvailable)}; + // the main circuit, also for a compressor driven by the engine, otherwise the low voltage + auto const compressorpower{CompressorPower == 0 || CompressorPower == 3 ? Mains : Power110vIsAvailable}; + CompressorPowerTime = compressorpower ? CompressorPowerTime + dt : 0.0; + auto const compressorready{compressorpower && CompressorPowerTime >= CompressorStartDelay}; // TBD: split CompressorAllow into separate enable/disable flags, inherit compressor from basic_device auto const compressorenable{CompressorAllowLocal && (CompressorStart == start_t::automatic || CompressorAllow)}; auto const compressordisable{false == compressorenable}; @@ -4354,7 +4403,7 @@ void TMoverParameters::CompressorCheck(double dt) auto const compressorflag{CompressorFlag}; CompressorFlag = - compressorpower && false == compressordisable && (false == governorlock || CompressorPower == 3) && (CompressorFlag || (compressorenable && LastSwitchingTime > CtrlDelay)); + compressorready && false == compressordisable && (false == governorlock || CompressorPower == 3) && (CompressorFlag || (compressorenable && LastSwitchingTime > CtrlDelay)); if (CompressorFlag && CompressorFlag != compressorflag) { @@ -4384,30 +4433,23 @@ void TMoverParameters::CompressorCheck(double dt) { // vent some air out if there's no governor lock to stop the compressor from exceeding acceptable pressure level SetFlag(SoundFlag, sound::relay | sound::loud); - CompressedVolume *= false == governorlockispresent ? 0.80 : // arbitrary amount - CompressorTankValve ? MinCompressorF / MaxCompressorF : // drop to mincompressor level - 0.999; // HACK: drop a tiny bit so the sound doesn't trigger repeatedly + auto ventedfraction{0.999}; // HACK: drop a tiny bit so the sound doesn't trigger repeatedly + if (false == governorlockispresent) + { + ventedfraction = 0.80; // arbitrary amount + } + else if (CompressorTankValve) + { + ventedfraction = MinCompressorF / MaxCompressorF; // drop to mincompressor level + } + CompressedVolume *= ventedfraction; if (false == governorlockispresent || CompressorTankValve) { CompressorGovernorLock = false; } } - // tymczasowo tylko obciążenie sprężarki, tak z 5A na sprężarkę - // TODO: draw power from proper high- or low voltage circuit - if (CompressorPower == 3) - { - // diesel-powered compressor doesn't draw power - } - else - { - // TODO: drain power from 110v circuit - /* - if( compressorowner != nullptr ) { - compressorowner->TotalCurrent += 0.0015 * compressorowner->PantographVoltage; - } - */ - } + // a compressor not driven by the engine doesn't draw power (some 5 A) from the high- or low voltage circuit yet } // ************************************************************************************************* @@ -5008,7 +5050,7 @@ void TMoverParameters::ComputeTotalForce(double dt) } else { - EngineVoltage *= DirAbsolute; // DirActive*CabActive; + EngineVoltage *= DirAbsolute; // the reverser of the active cab, relative to the couplers } } // bo nie dzialalo else @@ -5032,6 +5074,14 @@ void TMoverParameters::ComputeTotalForce(double dt) { SlippingWheels = true; } + if (true == SlippingWheels && Vel < 0.1 // the same crude trap: (nearly) stopped vehicle... + && std::abs(nrot * M_PI * WheelDiameter - V) < 0.1 / 3.6) // ...with wheels turning at its speed, i.e. locked wheels at the end of a skid + { + // locked wheels never pass the rotation speed test below once the vehicle stops, and a skidding vehicle + // keeps its braking force capped by the sliding adhesion, so the rest of the consist could push it around + SlippingWheels = false; + nrot = V / M_PI / WheelDiameter; + } double temp_nrot = nrot; if (true == SlippingWheels) { @@ -5286,7 +5336,7 @@ double TMoverParameters::CouplerForce(int const End, double dt) auto const newdistance{initialdistance + 10.0 * distancedelta}; - auto const dV{V - othervehicle->V * DirPatch(End, otherend)}; + auto const dV{CouplerVelocityDifference(End, *othervehicle, otherend)}; auto const absdV{std::abs(dV)}; // potentially generate sounds on clash or stretch @@ -5399,25 +5449,23 @@ double TMoverParameters::CouplerForce(int const End, double dt) couplingtype &= ~coupling::control; } - if (Attach(End, otherend, othervehicle, couplingtype)) - { - // HACK: we're reusing sound enum to mark whether vehicle was connected to another - SetFlag(AIFlag, sound::attachcoupler); - thiscoupler.AutomaticCouplingAllowed = false; - othercoupler.AutomaticCouplingAllowed = false; - } - /* - coupler.CouplingFlag = ( coupler.AutomaticCouplingFlag & othercoupler.AutomaticCouplingFlag ); - SetFlag( coupler.sounds, sound::attachcoupler ); - */ + // too fast for the couplers to catch, the vehicles collide instead (and may derail, see CollisionDetect()) + auto const safe{AutomaticCouplingSafe(End, *othervehicle, otherend)}; + auto const caught{safe && Attach(End, otherend, othervehicle, couplingtype)}; + // HACK: we're reusing sound enum to mark whether vehicle was connected to another + AIFlag |= (caught ? sound::attachcoupler : 0); + // coupled, or locked until the vehicles part, so they don't couple as soon as the collision slows them down + thiscoupler.AutomaticCouplingAllowed = safe && false == caught; + othercoupler.AutomaticCouplingAllowed = safe && false == caught; } } } if (thiscoupler.CouplingFlag != coupling::faux) { - // uzgadnianie prawa Newtona - othervehicle->Couplers[1 - End].CForce = -CF; + // uzgadnianie prawa Newtona, on the coupler of the other vehicle this one is coupled with, along that vehicle's + // axis: turned around once more when the vehicles face each other + othervehicle->Couplers[otherend].CForce = -CF * DirPatch(End, otherend); } return CF; @@ -10468,6 +10516,7 @@ void TMoverParameters::LoadFIZ_Brake(std::string const &line) auto lookup = compressorpowers.find(extract_value("CompressorPower", line)); CompressorPower = lookup != compressorpowers.end() ? lookup->second : 1; } + extract_value(CompressorStartDelay, "CompressorStartDelay", line, ""); if (true == extract_value(AirLeakRate, "AirLeakRate", line, "")) { @@ -10684,7 +10733,7 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) for (int idx = 0; idx < 4; ++idx) { - extract_value(BrakeDelay[idx], "BDelay" + std::to_string(idx + 1), line, ""); + extract_value(BrakeDelay[idx], std::format("BDelay{}", idx + 1), line, ""); } // brakedelays, brakedelayflag { @@ -10780,6 +10829,16 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) // maksymalna predkosc dostepna na tarczce predkosciomierza extract_value(maxTachoSpeed, "MaxTachoSpeed", line, ""); + // horns + { + using enum horn_supply; + std::map> const hornsupplies{{"Pneumatic", air}, {"Electric", electric}}; + auto lookup = hornsupplies.find(extract_value("HornType", line)); + HornSupply = lookup != hornsupplies.end() ? lookup->second : air; + lookup = hornsupplies.find(extract_value("Horn3Type", line)); + Horn3Supply = lookup != hornsupplies.end() ? lookup->second : HornSupply; + } + // dynamicbrake { std::map> dynamicbrakes{{"Passive", dbrake_passive}, {"Switch", dbrake_switch}, {"Reversal", dbrake_reversal}, {"Automatic", dbrake_automatic}}; @@ -10939,14 +10998,15 @@ void TMoverParameters::LoadFIZ_Cntrl(std::string const &line) // ground relay { auto lookup = starts.find(extract_value("GroundRelayStart", line)); - GroundRelayStart = lookup != starts.end() ? lookup->second : TrainType == dt_EZT ? start_t::automatic : start_t::manual; + auto const defaultstart{TrainType == dt_EZT ? start_t::automatic : start_t::manual}; + GroundRelayStart = lookup != starts.end() ? lookup->second : defaultstart; } // converter overload relay { auto lookup = starts.find(extract_value("ConverterOverloadRelayStart", line)); - ConverterOverloadRelayStart = lookup != starts.end() ? lookup->second : - TrainType == dt_EZT ? start_t::converter : // relay activates when converter is switched off - start_t::manual; + // the relay of a multiple unit activates when the converter is switched off + auto const defaultstart{TrainType == dt_EZT ? start_t::converter : start_t::manual}; + ConverterOverloadRelayStart = lookup != starts.end() ? lookup->second : defaultstart; } } @@ -11339,6 +11399,7 @@ void TMoverParameters::LoadFIZ_Switches(std::string const &Input) extract_value(PantSwitchType, "Pantograph", Input, ""); extract_value(ConvSwitchType, "Converter", Input, ""); extract_value(StLinSwitchType, "MotorConnectors", Input, ""); + HeatingSwitchImpulse = ToLower(extract_value("TrainHeating", Input)) == "impulse"; // because people can't make up their minds whether it's "impulse" or "Impulse"... PantSwitchType = ToLower(PantSwitchType); ConvSwitchType = ToLower(ConvSwitchType); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index a07da795f..9b5dc64ac 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -120,8 +120,8 @@ bool is_curved(segment_data const &Path) void copy_text(char *Buffer, std::size_t const Size, std::string const &Text) { - std::strncpy(Buffer, Text.c_str(), Size - 1); - Buffer[Size - 1] = 0; + // as much of the text as fits, always terminated + Buffer[Text.copy(Buffer, Size - 1)] = 0; } std::string lower_case(std::string Text) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b784..8dfd831f9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1188,8 +1188,8 @@ void editor_mode::render_path_parameters(TTrack &Track) if (m_track_material_edited[i] != nullptr) continue; auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{}; - std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1); - m_track_materials[i].back() = '\0'; + auto &buffer{m_track_materials[i]}; + buffer[name.copy(buffer.data(), buffer.size() - 1)] = '\0'; } } for (int i = 0; i < 3; ++i) @@ -1238,8 +1238,7 @@ void editor_mode::render_path_circuits(TTrack &Track) { auto *track{&Track}; auto const copy = [](std::array &Buffer, std::string const &Text) { - std::strncpy(Buffer.data(), Text.c_str(), Buffer.size() - 1); - Buffer.back() = '\0'; + Buffer[Text.copy(Buffer.data(), Buffer.size() - 1)] = '\0'; }; auto const isolated_text = [&]() { std::string text; diff --git a/input/command.cpp b/input/command.cpp index d7966f37d..0ae67cfa5 100644 --- a/input/command.cpp +++ b/input/command.cpp @@ -405,6 +405,7 @@ commanddescription_sequence Commands_descriptions = { {"wiperswitchincrease", command_target::vehicle, command_mode::oneoff}, {"wiperswitchdecrease", command_target::vehicle, command_mode::oneoff}, {"lightsset", command_target::vehicle, command_mode::oneoff}, + {"oilfuelpumptoggle", command_target::vehicle, command_mode::oneoff}, }; // Maps of command and coresponding strings @@ -789,6 +790,7 @@ std::unordered_map const commandMap = { {"wiperswitchincrease", user_command::wiperswitchincrease}, {"wiperswitchdecrease", user_command::wiperswitchdecrease}, {"lightsset", user_command::lightsset}, + {"oilfuelpumptoggle", user_command::oilfuelpumptoggle}, {"none", user_command::none}}; } // namespace simulation diff --git a/input/command.h b/input/command.h index e03622a9f..ddacef4a0 100644 --- a/input/command.h +++ b/input/command.h @@ -403,6 +403,7 @@ enum class user_command wiperswitchincrease, wiperswitchdecrease, lightsset, + oilfuelpumptoggle, none = -1 }; diff --git a/input/drivermouseinput.cpp b/input/drivermouseinput.cpp index ab3d0aaf8..22562b856 100644 --- a/input/drivermouseinput.cpp +++ b/input/drivermouseinput.cpp @@ -576,6 +576,9 @@ drivermouse_input::default_bindings() { { "oilpump_sw:", { oilpumptoggle, none } }, + { "oilfuelpump_sw:", { + oilfuelpumptoggle, + none } }, { "motorblowersfront_sw:", { motorblowerstogglefront, none } }, diff --git a/tests/unit/README.md b/tests/unit/README.md index bca46e83b..97cc94910 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -56,8 +56,10 @@ TEST_CASE("brakes: service braking stops a heavier wagon too") { functions on the vehicle state the class calls, and the test calls them the same way: see `vehicle/handover.h`, which `TController::TakeControl()` uses, and `test_handover.cpp`. - `vehicle` creates the vehicle the way `TDynamicObject::Init()` does and steps it the way the - simulation loop does, on a straight track of the given friction and gradient. Coupled vehicles - aren't supported yet. + simulation loop does, on a straight track of the given friction and gradient. `pair` puts two of + them on that track facing each other with the given ends and runs them together, as the + simulation runs the vehicles of a scene: the coupler and buffer forces, collisions and automatic + coupling come from the physics (`test_couplers.cpp`). Longer consists aren't supported yet. - Check behaviour (speeds, pressures, states the player sees) rather than internal flags, and make the test check its own preconditions (`REQUIRE` that the braking skidded, before checking what a skid leads to). diff --git a/tests/unit/test_cntrl.cpp b/tests/unit/test_cntrl.cpp new file mode 100644 index 000000000..0dd3ddc20 --- /dev/null +++ b/tests/unit/test_cntrl.cpp @@ -0,0 +1,39 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::vehicle; + +// the Cntrl. section of the vehicle definition, as read by TMoverParameters::LoadFIZ_Cntrl() + +TEST_CASE("cntrl: the brake delays of the settings come from BDelay1 to BDelay4") { + vehicle wagon{definition::load("wagon").set("Cntrl", "BDelay1", 10.0).set("Cntrl", "BDelay2", 20.0).set("Cntrl", "BDelay3", 30.0).set("Cntrl", "BDelay4", 40.0)}; + CHECK(wagon->BrakeDelay[0] == doctest::Approx(10.0)); + // the vehicle varies the others a little, by 0.83 to 0.9 (CheckLocomotiveParameters()) + for (auto setting{1}; setting < 4; ++setting) { + CAPTURE(setting); + auto const given{10.0 * (setting + 1)}; + CHECK(wagon->BrakeDelay[setting] >= 0.83 * given); + CHECK(wagon->BrakeDelay[setting] <= 0.9 * given); + } +} + +TEST_CASE("cntrl: the relays of a multiple unit start on their own, of other vehicles by hand") { + SUBCASE("multiple unit") { + vehicle unit{definition::load("wagon").set("Param", "Type", "ezt")}; + REQUIRE(unit->TrainType == dt_EZT); + CHECK(unit->GroundRelayStart == start_t::automatic); + CHECK(unit->ConverterOverloadRelayStart == start_t::converter); // activates when the converter is switched off + } + SUBCASE("other vehicle") { + vehicle wagon{definition::load("wagon")}; + CHECK(wagon->GroundRelayStart == start_t::manual); + CHECK(wagon->ConverterOverloadRelayStart == start_t::manual); + } + SUBCASE("given in the definition") { + vehicle unit{definition::load("wagon").set("Param", "Type", "ezt").set("Cntrl", "GroundRelayStart", "Manual").set("Cntrl", "ConverterOverloadRelayStart", "Automatic")}; + CHECK(unit->GroundRelayStart == start_t::manual); + CHECK(unit->ConverterOverloadRelayStart == start_t::automatic); + } +} diff --git a/tests/unit/test_compressor.cpp b/tests/unit/test_compressor.cpp new file mode 100644 index 000000000..c5aa2fff7 --- /dev/null +++ b/tests/unit/test_compressor.cpp @@ -0,0 +1,115 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::vehicle; + +// the compressor waiting a while after getting its power before it starts (CompressorStartDelay, +// MaSzyna-EU07/maszyna#67): in a diesel it starts some time after the engine, rather than with it + +namespace { + +double const step{0.01}; + +// a compressor which starts on its own, fed from the main circuit unless given another Power, its pressure +// switch between MinCP and MaxCP, and an empty main reservoir for it to fill +vehicle compressor_vehicle(double const Delay, std::string const &Power = "Main", double const MinCP = 6.5) { + auto definition{definition::load("wagon")}; + definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", MinCP).set("Brake", "MaxCP", 8.0); + definition.set("Brake", "CompressorSpeed", 0.01); + if (false == Power.empty()) { + definition.set("Brake", "CompressorPower", Power); + } + definition.set("Cntrl", "CompressorStart", "Automatic"); + if (Delay > 0.0) { + definition.set("Brake", "CompressorStartDelay", Delay); + } + vehicle vehicle{definition}; + vehicle->CompressedVolume = 0.0; + vehicle->LastSwitchingTime = 10.0; // nothing was switched lately + return vehicle; +} + +// runs the compressor for Seconds, the main circuit as it is, the time since the last switching going on as in +// ComputeTotalForce() +void run_compressor(vehicle &Vehicle, double const Seconds) { + for (auto i{std::lround(Seconds / step)}; i > 0; --i) { + Vehicle->LastSwitchingTime += step; + Vehicle->CompressorCheck(step); + } +} + +} // namespace + +TEST_CASE("compressor: without a start delay the compressor starts with its power") { + auto vehicle{compressor_vehicle(0.0)}; + run_compressor(vehicle, 1.0); + REQUIRE_FALSE(vehicle->CompressorFlag); // no power, no compressor + + vehicle->Mains = true; + run_compressor(vehicle, step); + CHECK(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: the compressor starts the delay after getting its power") { + auto vehicle{compressor_vehicle(5.0)}; + vehicle->Mains = true; + + run_compressor(vehicle, 4.9); + CHECK_FALSE(vehicle->CompressorFlag); + CHECK(vehicle->Compressor == doctest::Approx(0.0)); + + run_compressor(vehicle, 0.2); + CHECK(vehicle->CompressorFlag); + run_compressor(vehicle, 10.0); + CHECK(vehicle->Compressor > 0.0); // and fills the reservoir +} + +TEST_CASE("compressor: the delay starts over once the power is gone") { + auto vehicle{compressor_vehicle(5.0)}; + vehicle->Mains = true; + run_compressor(vehicle, 10.0); + REQUIRE(vehicle->CompressorFlag); + + vehicle->Mains = false; // the engine stopped + run_compressor(vehicle, 1.0); + CHECK_FALSE(vehicle->CompressorFlag); + + vehicle->Mains = true; // and started again + run_compressor(vehicle, 4.9); + CHECK_FALSE(vehicle->CompressorFlag); + run_compressor(vehicle, 0.2); + CHECK(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: a compressor fed from the low voltage waits from the low voltage coming on") { + auto vehicle{compressor_vehicle(5.0, "")}; // the default supply: the converter, the 110 V + vehicle->Mains = true; // which alone doesn't feed it + run_compressor(vehicle, 10.0); + REQUIRE_FALSE(vehicle->CompressorFlag); + + vehicle->Power110vIsAvailable = true; + run_compressor(vehicle, 4.9); + CHECK_FALSE(vehicle->CompressorFlag); + run_compressor(vehicle, 0.2); + CHECK(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: the pressure switch stops the compressor above the cut-out pressure") { + auto vehicle{compressor_vehicle(0.0)}; + vehicle->Mains = true; + vehicle->CompressedVolume = 8.5; // over MaxCP, in the reservoir of 1 m3 + run_compressor(vehicle, step); + CHECK(vehicle->CompressorGovernorLock); + CHECK_FALSE(vehicle->CompressorFlag); +} + +TEST_CASE("compressor: without a pressure switch the air over the cut-out pressure is vented") { + auto vehicle{compressor_vehicle(0.0, "Main", 8.0)}; // MinCP the same as MaxCP: no pressure switch + vehicle->Mains = true; + vehicle->CompressedVolume = 8.5; + run_compressor(vehicle, step); + REQUIRE(vehicle->CompressorFlag); // nothing stops it + CHECK(vehicle->CompressedVolume < 8.5 * 0.81); // a fifth of the air let out +} diff --git a/tests/unit/test_couplers.cpp b/tests/unit/test_couplers.cpp new file mode 100644 index 000000000..8787a419b --- /dev/null +++ b/tests/unit/test_couplers.cpp @@ -0,0 +1,458 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "utilities/Globals.h" + +using test::definition; +using test::vehicle; + +namespace { + +// the wagon from data/wagon.json with automatic couplers at both ends, which couple the given connections +definition automatic_definition(int Flag = coupling::coupler | coupling::brakehose) { + return definition::load("wagon").set("BuffCoupl", "CType", "Automatic").set("BuffCoupl", "AutomaticFlag", Flag); +} + +// the wagon from data/wagon.json with automatic couplers at both ends, running at the given speed [km/h] +vehicle automatic_wagon(double Velocity, std::string const &Name) { + return vehicle{automatic_definition(), Velocity, Name}; +} + +// the same wagon, twice as heavy +vehicle heavy_automatic_wagon(double Velocity, std::string const &Name) { + return vehicle{automatic_definition().set("Param", "M", 2 * 42200), Velocity, Name}; +} + +bool derailed(vehicle &Vehicle) { + return TestFlag(Vehicle->DamageFlag, dtrain_out); +} + +// runs the pair for Seconds, returns whether the vehicles were coupled at any point +bool couple_during(test::pair &Pair, double Seconds) { + auto coupled{false}; + for (auto i{std::lround(Seconds / 0.01)}; i > 0; --i) { + Pair.step(); + coupled = coupled || Pair.coupled(); + } + return coupled; +} + +// sets the crash damage setting for the test, restores it after +class crash_damage_setting { +public: + explicit crash_damage_setting(bool Enabled) { Global.crash_damage = Enabled; } + ~crash_damage_setting() { Global.crash_damage = m_previous; } + crash_damage_setting(crash_damage_setting const &) = delete; + crash_damage_setting &operator=(crash_damage_setting const &) = delete; + +private: + bool m_previous{Global.crash_damage}; +}; + +} // namespace + +// issue #86: automatic couplers used to catch at any closing speed, so the vehicles never collided nor derailed + +TEST_CASE("couplers: automatic couplers catch a standing wagon hit at a walking pace") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(10.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + REQUIRE(moving->Couplers[end::front].type() == TCouplerType::Automatic); + REQUIRE(moving.speed() == doctest::Approx(10.0)); + + // the front of the moving wagon runs into the rear of the standing one + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(10.0)); + CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + CHECK(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front)); +} + +TEST_CASE("couplers: automatic couplers don't catch a wagon hit too fast, the wagons collide instead") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + REQUIRE(moving.speed() == doctest::Approx(30.0)); + + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) > TMoverParameters::SafeCollisionVelocity); + CHECK_FALSE(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + CHECK_FALSE(standing->AutomaticCouplingSafe(end::rear, moving.mover(), end::front)); +} + +TEST_CASE("couplers: automatic couplers catch a wagon running ahead only a little slower") { + crash_damage_setting const crashdamage{true}; + auto faster{automatic_wagon(60.0, "faster")}; + auto slower{automatic_wagon(50.0, "slower")}; + + CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(10.0)); + CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(10.0)); + CHECK(faster->AutomaticCouplingSafe(end::front, slower.mover(), end::rear)); +} + +TEST_CASE("couplers: without crash damage automatic couplers catch at any speed") { + crash_damage_setting const crashdamage{false}; + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + + CHECK(moving->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); +} + +// the closing speed used to treat the speed of each vehicle as a vector turned by half its heading, so for vehicles +// facing each other it came out as the root of the sum of the squares of their speeds, rather than the sum + +TEST_CASE("couplers: the closing speed of a rear-end collision is the difference of the speeds") { + auto faster{automatic_wagon(40.0, "faster")}; + auto slower{automatic_wagon(25.0, "slower")}; + + // running forwards, the front of the faster wagon hits the rear of the slower one + CHECK(faster->CollisionVelocity(end::front, slower.mover(), end::rear) == doctest::Approx(15.0)); + CHECK(slower->CollisionVelocity(end::rear, faster.mover(), end::front) == doctest::Approx(15.0)); + // the wagons running ahead of each other are parting + CHECK(slower->CollisionVelocity(end::front, faster.mover(), end::rear) == doctest::Approx(-15.0)); + + // backing up, the rear of the faster wagon hits the front of the slower one + auto backingfaster{automatic_wagon(-40.0, "backing faster")}; + auto backingslower{automatic_wagon(-25.0, "backing slower")}; + CHECK(backingfaster->CollisionVelocity(end::rear, backingslower.mover(), end::front) == doctest::Approx(15.0)); + CHECK(backingslower->CollisionVelocity(end::front, backingfaster.mover(), end::rear) == doctest::Approx(15.0)); +} + +TEST_CASE("couplers: the closing speed of a head-on collision is the sum of the speeds") { + crash_damage_setting const crashdamage{true}; + auto first{automatic_wagon(10.0, "first")}; + auto second{automatic_wagon(10.0, "second")}; + + // both wagons run forwards, their fronts meet + CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) == doctest::Approx(20.0)); + CHECK(second->CollisionVelocity(end::front, first.mover(), end::front) == doctest::Approx(20.0)); + // 20 km/h is above the limit, the couplers don't catch (the old formula gave 14.1 km/h and coupled them) + CHECK(first->CollisionVelocity(end::front, second.mover(), end::front) > TMoverParameters::SafeCollisionVelocity); + CHECK_FALSE(first->AutomaticCouplingSafe(end::front, second.mover(), end::front)); + CHECK_FALSE(second->AutomaticCouplingSafe(end::front, first.mover(), end::front)); + + // both wagons back up, their rears meet + auto firstbacking{automatic_wagon(-10.0, "first backing")}; + auto secondbacking{automatic_wagon(-10.0, "second backing")}; + CHECK(firstbacking->CollisionVelocity(end::rear, secondbacking.mover(), end::rear) == doctest::Approx(20.0)); + + // running back to back, the wagons part + CHECK(first->CollisionVelocity(end::rear, second.mover(), end::rear) == doctest::Approx(-20.0)); + CHECK(first->AutomaticCouplingSafe(end::rear, second.mover(), end::rear)); + + // slow enough, the couplers catch + auto slowfirst{automatic_wagon(5.0, "slow first")}; + auto slowsecond{automatic_wagon(5.0, "slow second")}; + CHECK(slowfirst->CollisionVelocity(end::front, slowsecond.mover(), end::front) == doctest::Approx(10.0)); + CHECK(slowfirst->AutomaticCouplingSafe(end::front, slowsecond.mover(), end::front)); +} + +TEST_CASE("couplers: the closing speed of a hit on a standing vehicle is the speed of the moving one, whichever way it faces") { + auto moving{automatic_wagon(20.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + + // the standing wagon faces away from the moving one + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(20.0)); + CHECK(standing->CollisionVelocity(end::rear, moving.mover(), end::front) == doctest::Approx(20.0)); + // the standing wagon faces the moving one + CHECK(moving->CollisionVelocity(end::front, standing.mover(), end::front) == doctest::Approx(20.0)); + CHECK(standing->CollisionVelocity(end::front, moving.mover(), end::front) == doctest::Approx(20.0)); + // the moving wagon backs up into the standing one + auto backing{automatic_wagon(-20.0, "backing")}; + CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(20.0)); + CHECK(backing->CollisionVelocity(end::rear, standing.mover(), end::rear) == doctest::Approx(20.0)); + // the moving wagon runs away from the standing one + CHECK(moving->CollisionVelocity(end::rear, standing.mover(), end::front) == doctest::Approx(-20.0)); +} + +// the coupling as the simulation runs it: two vehicles on a track, closing in on each other until the couplers (or +// buffers) meet, the coupler forces and the collision computed by the physics, step by step + +TEST_CASE("couplers: automatic couplers catch right up to the safe closing speed") { + crash_damage_setting const crashdamage{true}; + auto standing{automatic_wagon(0.0, "standing")}; + + auto below{automatic_wagon(14.9, "below")}; + CHECK(below->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(14.9)); + CHECK(below->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + + auto above{automatic_wagon(15.1, "above")}; + CHECK(above->CollisionVelocity(end::front, standing.mover(), end::rear) == doctest::Approx(15.1)); + CHECK_FALSE(above->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); + auto faster{automatic_wagon(15.5, "faster")}; + CHECK_FALSE(faster->AutomaticCouplingSafe(end::front, standing.mover(), end::rear)); +} + +TEST_CASE("couplers: a wagon rolling slowly into a standing one couples with it") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(5.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + pair.run(3.0); + + REQUIRE(pair.coupled()); + CHECK(moving->Couplers[end::front].CouplingFlag == (coupling::coupler | coupling::brakehose)); + CHECK(standing->Couplers[end::rear].CouplingFlag == (coupling::coupler | coupling::brakehose)); + CHECK(moving->Couplers[end::front].Connected == &standing.mover()); + CHECK_FALSE(derailed(moving)); + CHECK_FALSE(derailed(standing)); + // coupled, they run on together + pair.run(2.0); + CHECK(moving.speed() == doctest::Approx(standing.speed()).epsilon(0.05)); + CHECK(pair.gap() < 0.2); +} + +TEST_CASE("couplers: a wagon hitting a standing one too fast doesn't couple, nor once the hit has slowed it down") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + auto touched{false}; + auto parted{false}; + auto coupled{false}; + auto locked{true}; + for (int i = 0; i < 1000; ++i) { + pair.step(); + // as far as the couplers know: the distance between them at the start of the step + auto const distance{moving->Neighbours[end::front].distance}; + touched = touched || distance < 0.0; + parted = parted || (touched && distance > 0.05); + coupled = coupled || pair.coupled(); + if (touched && false == parted) { + locked = locked && false == moving->Couplers[end::front].AutomaticCouplingAllowed && false == standing->Couplers[end::rear].AutomaticCouplingAllowed; + } + } + + REQUIRE(touched); + CHECK_FALSE(coupled); + // the hit derailed and stopped them, the couplers next to each other, the automatic coupling locked all along + CHECK_FALSE(parted); + CHECK(locked); + CHECK(derailed(moving)); + CHECK(derailed(standing)); + CHECK(std::abs(moving.speed()) < 3.0); + CHECK(std::abs(standing.speed()) < 3.0); + CHECK_FALSE(couple_during(pair, 5.0)); +} + +TEST_CASE("couplers: the automatic coupling locked by a hit is unlocked once the vehicles part") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(3.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + // as a hit too fast or manual uncoupling leaves them + moving->Couplers[end::front].AutomaticCouplingAllowed = false; + standing->Couplers[end::rear].AutomaticCouplingAllowed = false; + + SUBCASE("parted by more than 5 cm, they couple on the next approach") { + test::pair pair{moving, end::front, standing, end::rear, 0.5}; + pair.step(); + CHECK(moving->Couplers[end::front].AutomaticCouplingAllowed); + CHECK(couple_during(pair, 3.0)); + } + SUBCASE("kept together, they don't") { + test::pair pair{moving, end::front, standing, end::rear, 0.0}; + auto coupled{false}; + for (int i = 0; i < 300; ++i) { + pair.step(); + if (pair.gap() > 0.05) { + break; // parted + } + coupled = coupled || pair.coupled(); + } + CHECK_FALSE(coupled); + } +} + +// the couplers refuse a hit by the closing speed when they meet, the derail check (CollisionDetect()) takes the one +// left when the buffers are pressed in all the way, after their springs have slowed the hit down: for the wagons here +// by about 7 km/h. so a hit a little over the safe closing speed only bounces the vehicles apart + +TEST_CASE("couplers: a hit a little too fast for the couplers bounces the vehicles apart, uncoupled and on the rails") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(20.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + CHECK_FALSE(couple_during(pair, 3.0)); + CHECK_FALSE(derailed(moving)); + CHECK_FALSE(derailed(standing)); + CHECK(pair.gap() > 0.5); + CHECK(standing.speed() > moving.speed()); +} + +TEST_CASE("couplers: a hit too fast for the couplers derails the vehicles over the limit adjusted for their mass ratio") { + crash_damage_setting const crashdamage{true}; + + SUBCASE("equal masses: both derail") { + auto moving{automatic_wagon(30.0, "moving")}; + auto standing{automatic_wagon(0.0, "standing")}; + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(moving)); + CHECK(derailed(standing)); + } + SUBCASE("half the mass: between the safe closing speed and twice that, only the lighter one derails") { + auto light{automatic_wagon(27.0, "light")}; + auto heavy{heavy_automatic_wagon(0.0, "heavy")}; + test::pair pair{light, end::front, heavy, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(light)); + CHECK_FALSE(derailed(heavy)); + } + SUBCASE("half the mass: above twice the safe closing speed both derail") { + auto light{automatic_wagon(45.0, "light")}; + auto heavy{heavy_automatic_wagon(0.0, "heavy")}; + test::pair pair{light, end::front, heavy, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(light)); + CHECK(derailed(heavy)); + } + SUBCASE("half the mass: below the safe closing speed the couplers catch, nothing derails") { + auto light{automatic_wagon(12.0, "light")}; + auto heavy{heavy_automatic_wagon(0.0, "heavy")}; + test::pair pair{light, end::front, heavy, end::rear, 1.0}; + CHECK(couple_during(pair, 5.0)); + CHECK_FALSE(derailed(light)); + CHECK_FALSE(derailed(heavy)); + } +} + +TEST_CASE("couplers: wagons running head-on into each other couple only up to the safe closing speed") { + crash_damage_setting const crashdamage{true}; + + SUBCASE("7 + 7 km/h, the couplers catch") { + auto first{automatic_wagon(7.0, "first")}; + auto second{automatic_wagon(7.0, "second")}; + test::pair pair{first, end::front, second, end::front, 1.0}; + CHECK(couple_during(pair, 3.0)); + CHECK_FALSE(derailed(first)); + CHECK_FALSE(derailed(second)); + } + SUBCASE("8 + 8 km/h, the wagons bounce apart") { + auto first{automatic_wagon(8.0, "first")}; + auto second{automatic_wagon(8.0, "second")}; + test::pair pair{first, end::front, second, end::front, 1.0}; + CHECK_FALSE(couple_during(pair, 3.0)); + CHECK_FALSE(derailed(first)); + CHECK_FALSE(derailed(second)); + // both backwards, away from each other + CHECK(first.speed() < 0.0); + CHECK(second.speed() < 0.0); + } + SUBCASE("backing up, 8 + 8 km/h, the wagons bounce apart") { + auto first{automatic_wagon(-8.0, "first")}; + auto second{automatic_wagon(-8.0, "second")}; + test::pair pair{first, end::rear, second, end::rear, 1.0}; + CHECK_FALSE(couple_during(pair, 3.0)); + CHECK(first.speed() > 0.0); + CHECK(second.speed() > 0.0); + } + SUBCASE("15 + 15 km/h, both derail") { + auto first{automatic_wagon(15.0, "first")}; + auto second{automatic_wagon(15.0, "second")}; + test::pair pair{first, end::front, second, end::front, 1.0}; + CHECK_FALSE(couple_during(pair, 5.0)); + CHECK(derailed(first)); + CHECK(derailed(second)); + } +} + +// the collision used to take the speed of a vehicle facing the other way as if it faced the same way, so a wagon +// pushed away by the buffers of the vehicle hitting it head-on counted as running into it: the hit slowed it down, +// and the hitter bounced back as from a wall + +TEST_CASE("couplers: a wagon hit head-on runs off the same as one hit from behind") { + // without automatic couplers or crash damage, the collision alone + crash_damage_setting const crashdamage{false}; + auto speeds_after_hit = [](int StruckEnd) { + auto moving{vehicle{definition::load("wagon"), 20.0, "moving"}}; + auto struck{vehicle{definition::load("wagon"), 0.0, "struck"}}; + test::pair pair{moving, end::front, struck, StruckEnd, 0.5}; + auto touched{false}; + for (int i = 0; i < 300; ++i) { + pair.step(); + touched = touched || pair.gap() < 0.0; + if (touched && pair.gap() > 0.3) { + break; // bounced off + } + } + REQUIRE(touched); + REQUIRE(pair.gap() > 0.3); + REQUIRE_FALSE(pair.coupled()); + return std::make_pair(moving.speed(), struck.speed()); + }; + + auto const [hitterfrombehind, struckfrombehind]{speeds_after_hit(end::rear)}; + auto const [hitterheadon, struckheadon]{speeds_after_hit(end::front)}; + + // hit from behind, the struck wagon runs off forwards, faster than the hitter + CHECK(struckfrombehind > 10.0); + CHECK(hitterfrombehind < struckfrombehind); + // hit head-on, it runs off backwards, as fast + CHECK(struckheadon == doctest::Approx(-struckfrombehind).epsilon(0.02)); + CHECK(hitterheadon == doctest::Approx(hitterfrombehind).epsilon(0.05)); +} + +// the other conditions of automatic coupling + +TEST_CASE("couplers: an automatic coupler doesn't catch a coupler of another type") { + crash_damage_setting const crashdamage{true}; + auto moving{automatic_wagon(5.0, "moving")}; + auto standing{vehicle{definition::load("wagon"), 0.0, "standing"}}; + REQUIRE(standing->Couplers[end::rear].type() == TCouplerType::Screw); + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + + CHECK_FALSE(couple_during(pair, 3.0)); +} + +TEST_CASE("couplers: automatic couplers of different control systems couple without the control connection") { + crash_damage_setting const crashdamage{true}; + auto const flag{coupling::coupler | coupling::brakehose | coupling::control}; + auto moving{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 5.0, "moving"}}; + + SUBCASE("the same control system") { + auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "first"), 0.0, "standing"}}; + REQUIRE(standing->Couplers[end::rear].control_type == "first"); + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + REQUIRE(couple_during(pair, 3.0)); + CHECK(pair.coupling() == flag); + } + SUBCASE("a different one") { + auto standing{vehicle{automatic_definition(flag).set("BuffCoupl", "ControlType", "second"), 0.0, "standing"}}; + REQUIRE(standing->Couplers[end::rear].control_type == "second"); + test::pair pair{moving, end::front, standing, end::rear, 1.0}; + REQUIRE(couple_during(pair, 3.0)); + CHECK(pair.coupling() == (coupling::coupler | coupling::brakehose)); + } +} + +TEST_CASE("couplers: the coupler force of vehicles facing each other lands on the couplers holding them") { + // the force a vehicle takes from its coupler, the other one takes back on its own coupler of the pair, whichever + // way the vehicles face; it used to land on the far end of the other vehicle when they faced each other. + // CForce runs along the axis of its own vehicle, so the two forces of the pair are opposite along the track: of + // opposite signs for vehicles facing the same way, of the same sign for vehicles facing each other + auto first{automatic_wagon(5.0, "first")}; + auto second{automatic_wagon(0.0, "second")}; + + SUBCASE("facing each other") { + test::pair pair{first, end::front, second, end::front, 1.0}; + pair.run(5.0); + REQUIRE(pair.coupled()); + + pair.step(); + REQUIRE(second->Couplers[end::front].CForce != 0.0); + CHECK(first->Couplers[end::front].CForce == doctest::Approx(second->Couplers[end::front].CForce)); + CHECK(first->Couplers[end::rear].CForce == 0.0); // nothing there + CHECK(second->Couplers[end::rear].CForce == 0.0); + } + SUBCASE("facing the same way") { + test::pair pair{first, end::front, second, end::rear, 1.0}; + pair.run(5.0); + REQUIRE(pair.coupled()); + + pair.step(); + REQUIRE(second->Couplers[end::rear].CForce != 0.0); + CHECK(first->Couplers[end::front].CForce == doctest::Approx(-second->Couplers[end::rear].CForce)); + CHECK(first->Couplers[end::rear].CForce == 0.0); + CHECK(second->Couplers[end::front].CForce == 0.0); + } +} diff --git a/tests/unit/test_gauge_positions.cpp b/tests/unit/test_gauge_positions.cpp new file mode 100644 index 000000000..791234e9d --- /dev/null +++ b/tests/unit/test_gauge_positions.cpp @@ -0,0 +1,58 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle/gaugepositions.h" + +#include + +// the sounds of the positions of a cab control (MaSzyna-EU07/maszyna#83): a brake lever moved smoothly (Num3/Num9) +// passes its positions instead of stopping at them, and sounds each one it reaches, as it does when put there + +namespace { + +// a brake lever with sounds at positions 0, 2 and 5; values in hundredths of a position +std::map lever_sounds() { + return {{0, "release"}, {2, "running"}, {5, "service"}}; +} + +std::string reached(int const Previous, int const Desired) { + auto positions{lever_sounds()}; + auto const position{gauge_positions::last_reached(positions, Previous, Desired)}; + return position != positions.end() ? position->second : ""; +} + +} // namespace + +TEST_CASE("gauge positions: a lever put at a position sounds it") { + CHECK(reached(100, 200) == "running"); + CHECK(reached(600, 500) == "service"); +} + +TEST_CASE("gauge positions: a lever moved smoothly past a position sounds it (MaSzyna-EU07/maszyna#83)") { + CHECK(reached(190, 205) == "running"); // moving up + CHECK(reached(510, 495) == "service"); // moving down +} + +TEST_CASE("gauge positions: of the positions passed in one go the last one reached sounds") { + CHECK(reached(50, 520) == "service"); // up past 2 and 5 + CHECK(reached(520, 10) == "running"); // down past 5 and 2, short of 0 + CHECK(reached(520, 0) == "release"); +} + +TEST_CASE("gauge positions: moving between the positions or off one sounds nothing") { + CHECK(reached(210, 290) == ""); + CHECK(reached(200, 230) == ""); // leaving a position isn't reaching it + CHECK(reached(200, 170) == ""); +} + +TEST_CASE("gauge positions: positions below zero count the same way") { + // a brake lever with the release position at -1 + std::map positions{{-1, "release"}, {0, "running"}}; + CHECK(gauge_positions::last_reached(positions, 50, -120)->second == "release"); // down past 0 and -1 + CHECK(gauge_positions::last_reached(positions, -150, -90)->second == "release"); // up past -1 +} + +TEST_CASE("gauge positions: a control without position sounds reaches none") { + std::map positions; + CHECK(gauge_positions::last_reached(positions, 0, 500) == positions.end()); +} diff --git a/tests/unit/test_horn.cpp b/tests/unit/test_horn.cpp new file mode 100644 index 000000000..18e5c5322 --- /dev/null +++ b/tests/unit/test_horn.cpp @@ -0,0 +1,121 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle.h" + +#include "vehicle/horn.h" + +// the horn keys of the cab: low tone (A) and high tone (shift+A), held to sound + +TEST_CASE("horn: a single lever for both tones sounds one tone at a time (eu07/maszyna#392)") { + auto signal{0}; + signal = horn::pressed(signal, horn::low, true); + REQUIRE(signal == horn::low); + + // the lever moved over to the high tone while the low tone key is still held + signal = horn::pressed(signal, horn::high, true); + CHECK(signal == horn::high); + + // letting go of the low tone key leaves the high tone sounding + signal = horn::released(signal, horn::low); + CHECK(signal == horn::high); + + signal = horn::released(signal, horn::high); + CHECK(signal == 0); +} + +TEST_CASE("horn: separate buttons for the tones can sound both at once") { + auto signal{0}; + signal = horn::pressed(signal, horn::low, false); + signal = horn::pressed(signal, horn::high, false); + CHECK(signal == (horn::low | horn::high)); + + signal = horn::released(signal, horn::low); + CHECK(signal == horn::high); +} + +TEST_CASE("horn: releasing a tone leaves the other bits of the signal alone") { + auto const third{4}; // the third horn, operated separately + CHECK(horn::released(horn::low | third, horn::low) == third); + CHECK(horn::pressed(third, horn::high, true) == (horn::high | third)); +} + +// what the horns need to sound (eu07/maszyna#290): a pneumatic horn the air of the main reservoir, an electric one +// the low voltage. the horn lever stays where the driver put it, only the horn is silent +namespace { + +test::vehicle horn_vehicle(test::definition Definition) { + // a main reservoir of its own + return test::vehicle{Definition.set("Brake", "Vv", 1.0).set("Brake", "MinCP", 6.5).set("Brake", "MaxCP", 8.0)}; +} + +int const allhorns{horn::low | horn::high | 4}; + +} // namespace + +TEST_CASE("horn: a pneumatic horn sounds on the air of the main reservoir only") { + auto vehicle{horn_vehicle(test::definition::load("wagon"))}; + REQUIRE(vehicle->HornSupply == horn_supply::air); // the default + + vehicle->Compressor = 6.5; + CHECK(vehicle->SoundingHorns(allhorns) == allhorns); + vehicle->Compressor = 0.0; // the reservoir is empty + CHECK(vehicle->SoundingHorns(allhorns) == 0); +} + +TEST_CASE("horn: a vehicle without a main reservoir takes the horn air from the main pipe") { + test::vehicle vehicle{test::definition::load("wagon")}; + REQUIRE(vehicle->VeselVolume == 0.0); + + vehicle->ScndPipePress = 5.0; + CHECK(vehicle->SoundingHorns(horn::low) == horn::low); + vehicle->ScndPipePress = 0.0; + CHECK(vehicle->SoundingHorns(horn::low) == 0); +} + +TEST_CASE("horn: an electric horn sounds on the low voltage, whatever the air") { + auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "HornType", "Electric"))}; + REQUIRE(vehicle->HornSupply == horn_supply::electric); + REQUIRE(vehicle->Horn3Supply == horn_supply::electric); // the third horn follows the others unless told otherwise + + vehicle->Compressor = 0.0; + vehicle->Power24vIsAvailable = true; + CHECK(vehicle->SoundingHorns(allhorns) == allhorns); + vehicle->Power24vIsAvailable = false; + CHECK(vehicle->SoundingHorns(allhorns) == 0); +} + +TEST_CASE("horn: the third horn can have a supply of its own, an electric horn of a diesel say") { + auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "Horn3Type", "Electric"))}; + REQUIRE(vehicle->HornSupply == horn_supply::air); + REQUIRE(vehicle->Horn3Supply == horn_supply::electric); + + vehicle->Compressor = 0.0; + vehicle->Power24vIsAvailable = true; + CHECK(vehicle->SoundingHorns(allhorns) == 4); + vehicle->Compressor = 6.5; + vehicle->Power24vIsAvailable = false; + CHECK(vehicle->SoundingHorns(allhorns) == (horn::low | horn::high)); +} + +TEST_CASE("horn: a vehicle without any air, a road vehicle say, sounds its pneumatic horn as before") { + // no main reservoir and no main pipe connection at either end + test::vehicle vehicle{test::definition::load("wagon").set("BuffCoupl", "AllowedFlag", 3)}; + REQUIRE(vehicle->VeselVolume == 0.0); + vehicle->ScndPipePress = 0.0; + CHECK(vehicle->SoundingHorns(allhorns) == allhorns); +} + +TEST_CASE("horn: a pneumatic horn sounds from the threshold pressure up") { + auto vehicle{horn_vehicle(test::definition::load("wagon"))}; + vehicle->Compressor = TMoverParameters::HornMinPressure; + CHECK(vehicle->SoundingHorns(horn::low) == horn::low); + vehicle->Compressor = TMoverParameters::HornMinPressure - 0.01; + CHECK(vehicle->SoundingHorns(horn::low) == 0); +} + +TEST_CASE("horn: an electric horn sounds on 110 V too") { + auto vehicle{horn_vehicle(test::definition::load("wagon").set("Cntrl", "HornType", "Electric"))}; + vehicle->Power110vIsAvailable = true; + CHECK(vehicle->SoundingHorns(horn::high) == horn::high); +} diff --git a/tests/unit/test_line_contactors.cpp b/tests/unit/test_line_contactors.cpp new file mode 100644 index 000000000..79c419edf --- /dev/null +++ b/tests/unit/test_line_contactors.cpp @@ -0,0 +1,59 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the lamp of the open line contactors (eu07/maszyna#470): lit, it tells the start can be made, so it should go out +// where the start is cut off, by the pressure switch at 2 bar in the brake cylinders, not at 1 bar already + +namespace { + +test::vehicle locomotive(bool const Pressureswitch) { + test::vehicle vehicle{test::definition::load("wagon").set("Engine", "PressureSwitch", Pressureswitch)}; + vehicle->StLinFlag = false; // the line contactors open + vehicle->PipePress = 5.0; + return vehicle; +} + +} // namespace + +TEST_CASE("line contactors: with a pressure switch the lamp goes out with the start cut off") { + auto vehicle{locomotive(true)}; + REQUIRE(vehicle->HasControlPressureSwitch); + + vehicle->BrakePress = 1.5; // braked, the start can still be made + vehicle->MotorConnectorsCheck(); // updates the pressure switch + REQUIRE_FALSE(vehicle->ControlPressureSwitch); + CHECK(vehicle->LineContactorsLampLit()); + + vehicle->BrakePress = 2.5; // the pressure switch cuts the start off + vehicle->MotorConnectorsCheck(); + REQUIRE(vehicle->ControlPressureSwitch); + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} + +TEST_CASE("line contactors: without a pressure switch the lamp goes out with the brakes applied, as before") { + auto vehicle{locomotive(false)}; + vehicle->BrakePress = 0.5; + vehicle->MotorConnectorsCheck(); + CHECK(vehicle->LineContactorsLampLit()); + vehicle->BrakePress = 1.5; + vehicle->MotorConnectorsCheck(); + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} + +TEST_CASE("line contactors: closed contactors put the lamp out") { + auto vehicle{locomotive(true)}; + vehicle->StLinFlag = true; + CHECK_FALSE(vehicle->LineContactorsLampLit()); +} + +TEST_CASE("line contactors: the ground relay and overload lamps follow the same rule") { + // all the lamps of the main circuit go out together, with the pressure switch where it's fitted + auto vehicle{locomotive(true)}; + vehicle->BrakePress = 1.5; + vehicle->MotorConnectorsCheck(); + CHECK(vehicle->MainCircuitLampsEnabled()); + vehicle->BrakePress = 2.5; + vehicle->MotorConnectorsCheck(); + CHECK_FALSE(vehicle->MainCircuitLampsEnabled()); +} diff --git a/tests/unit/test_mirrors.cpp b/tests/unit/test_mirrors.cpp new file mode 100644 index 000000000..7408eeaa5 --- /dev/null +++ b/tests/unit/test_mirrors.cpp @@ -0,0 +1,32 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle/mirrors.h" + +// the sounds of the mirrors (MaSzyna-EU07/maszyna#84, eu07/maszyna#322): a mirror sounds as it sets off, unfolding +// or folding, and not again while it keeps moving the same way + +using mirror_moves::event; + +TEST_CASE("mirrors: a mirror sounds once as it sets off") { + mirror_moves::motion mirror; + CHECK(mirror.update(0.0) == event::none); // standing folded + CHECK(mirror.update(0.01) == event::unfolding); + CHECK(mirror.update(0.01) == event::none); // still unfolding + CHECK(mirror.update(0.0) == event::none); // unfolded + CHECK(mirror.update(-0.01) == event::folding); + CHECK(mirror.update(-0.01) == event::none); +} + +TEST_CASE("mirrors: a mirror turned back halfway sounds the other way") { + mirror_moves::motion mirror; + REQUIRE(mirror.update(0.01) == event::unfolding); + CHECK(mirror.update(-0.01) == event::folding); // the vehicle got too fast while it was unfolding +} + +TEST_CASE("mirrors: a mirror setting off again the same way sounds again") { + mirror_moves::motion mirror; + REQUIRE(mirror.update(0.01) == event::unfolding); + REQUIRE(mirror.update(0.0) == event::none); // stopped, the doors lost their permit say + CHECK(mirror.update(0.01) == event::unfolding); +} diff --git a/tests/unit/test_pumps.cpp b/tests/unit/test_pumps.cpp new file mode 100644 index 000000000..2245e3eb8 --- /dev/null +++ b/tests/unit/test_pumps.cpp @@ -0,0 +1,52 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the joint oil and fuel pump switch (MaSzyna-EU07/maszyna#81) works the pumps which aren't started automatically + +namespace { + +test::vehicle pumps_vehicle(std::string const &Fuelstart, std::string const &Oilstart) { + return test::vehicle{test::definition::load("wagon").set("Cntrl", "FuelStart", Fuelstart).set("Cntrl", "OilStart", Oilstart)}; +} + +// what the two-state joint switch does when pressed, as TTrain::OnCommand_oilfuelpumptoggle() +void press_joint_switch(test::vehicle &Vehicle) { + auto const enable{false == Vehicle->OilFuelPumpsEnabled()}; + Vehicle->FuelPumpSwitch(enable, range_t::local); + Vehicle->FuelPumpSwitchOff(false == enable, range_t::local); + Vehicle->OilPumpSwitch(enable, range_t::local); + Vehicle->OilPumpSwitchOff(false == enable, range_t::local); +} + +} // namespace + +TEST_CASE("pumps: the joint switch turns both manual pumps on, then off") { + auto vehicle{pumps_vehicle("Manual", "Manual")}; + REQUIRE_FALSE(vehicle->OilFuelPumpsEnabled()); + press_joint_switch(vehicle); + CHECK(vehicle->FuelPump.is_enabled); + CHECK(vehicle->OilPump.is_enabled); + CHECK(vehicle->OilFuelPumpsEnabled()); + press_joint_switch(vehicle); + CHECK_FALSE(vehicle->FuelPump.is_enabled); + CHECK_FALSE(vehicle->OilPump.is_enabled); +} + +TEST_CASE("pumps: with one pump started automatically the joint switch still turns the other one off") { + auto vehicle{pumps_vehicle("Manual", "Automatic")}; + press_joint_switch(vehicle); + REQUIRE(vehicle->FuelPump.is_enabled); + CHECK(vehicle->OilFuelPumpsEnabled()); // the switch shows on + press_joint_switch(vehicle); + CHECK_FALSE(vehicle->FuelPump.is_enabled); +} + +TEST_CASE("pumps: with one manual pump on and the other off the joint switch turns both on") { + auto vehicle{pumps_vehicle("Manual", "Manual")}; + vehicle->FuelPumpSwitch(true, range_t::local); + REQUIRE_FALSE(vehicle->OilFuelPumpsEnabled()); + press_joint_switch(vehicle); + CHECK(vehicle->FuelPump.is_enabled); + CHECK(vehicle->OilPump.is_enabled); +} diff --git a/tests/unit/test_radio.cpp b/tests/unit/test_radio.cpp new file mode 100644 index 000000000..33a12f72d --- /dev/null +++ b/tests/unit/test_radio.cpp @@ -0,0 +1,28 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the radio works only switched on and with the low voltage (eu07/maszyna#429): its channel and volume buttons, +// the calls and the radio-stop all ask RadioPowered() + +TEST_CASE("radio: a radio switched on with the low voltage works") { + test::vehicle vehicle{test::definition::load("wagon")}; + vehicle->Radio = true; + SUBCASE("24 V") { vehicle->Power24vIsAvailable = true; } + SUBCASE("110 V") { vehicle->Power110vIsAvailable = true; } + CHECK(vehicle->RadioPowered()); +} + +TEST_CASE("radio: a radio switched off or without the low voltage doesn't") { + test::vehicle vehicle{test::definition::load("wagon")}; + SUBCASE("switched off") { + vehicle->Radio = false; + vehicle->Power24vIsAvailable = true; + } + SUBCASE("batteries off") { + vehicle->Radio = true; + vehicle->Power24vIsAvailable = false; + vehicle->Power110vIsAvailable = false; + } + CHECK_FALSE(vehicle->RadioPowered()); +} diff --git a/tests/unit/test_switches.cpp b/tests/unit/test_switches.cpp new file mode 100644 index 000000000..72b1e47ce --- /dev/null +++ b/tests/unit/test_switches.cpp @@ -0,0 +1,16 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// the kinds of the cab switches, given in the Switches: section of the vehicle definition + +TEST_CASE("switches: the train heating switch can spring back (MaSzyna-EU07/maszyna#85)") { + SUBCASE("impulse") { + test::vehicle vehicle{test::definition::load("cab_vehicle").set("Switches", "TrainHeating", "Impulse")}; + CHECK(vehicle->HeatingSwitchImpulse); // whichever the case it's written in + } + SUBCASE("a plain switch by default") { + test::vehicle vehicle{test::definition::load("cab_vehicle")}; + CHECK_FALSE(vehicle->HeatingSwitchImpulse); + } +} diff --git a/tests/unit/test_wheelslip.cpp b/tests/unit/test_wheelslip.cpp new file mode 100644 index 000000000..9b39e82c6 --- /dev/null +++ b/tests/unit/test_wheelslip.cpp @@ -0,0 +1,62 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::track; +using test::vehicle; + +namespace { + +struct braking_result { + bool skidded{false}; // the wheels locked while braking + bool stopped{false}; // came (almost) to a stop on the braking rail +}; + +// brakes the wagon from 40 km/h on the Braking rail until it's (nearly) stopped, then has it stand on the +// Standing one for 30 seconds +braking_result brake_to_stop_then_stand(vehicle &Coach, track const &Braking, track const &Standing) { + braking_result result; + Coach.hold_brake_pipe(5.0); + Coach.run(5.0, Braking); + Coach.hold_brake_pipe(3.5); // service braking + // a skidding wagon brakes on the sliding adhesion only, give it time + for (auto step = 0; step < 60000; ++step) { // 600 s in 0.01 s steps + result.skidded |= Coach->SlippingWheels; + if (auto const speed{std::abs(Coach.speed())}; speed > 0.0 && speed < 0.1) { + // almost stopped; a skidding wagon is still skidding at this point + result.stopped = true; + break; + } + Coach.step(Braking); + } + Coach.run(30.0, Standing); + return result; +} + +} // namespace + +// the engine doesn't let a stopped, braked vehicle slip (the Vel > 0.1 trap in TMoverParameters::ComputeTotalForce), +// so a wagon which stopped in a skid should be no different from one which stopped without it. it used to stay +// skidding instead, with its brakes capped by the sliding adhesion, and slid or crept away (eu07/maszyna#479) +TEST_CASE("wheelslip: a wagon which stopped in a skid holds like one which stopped without it") { + track const dry{0.15, 0.0}; + track const slippery{0.02, 0.0}; + // held only by the brakes: the sliding adhesion on this rail is too low for the slope + track const slipperyslope{0.02, 0.08}; + + vehicle normal{definition::load("wagon"), 40.0}; + auto const normalbraking{brake_to_stop_then_stand(normal, dry, slipperyslope)}; + vehicle skidding{definition::load("wagon"), 40.0}; + auto const skiddingbraking{brake_to_stop_then_stand(skidding, slippery, slipperyslope)}; + + // the test means something only when the braking went as planned + REQUIRE(normalbraking.stopped); + REQUIRE_FALSE(normalbraking.skidded); + REQUIRE(skiddingbraking.stopped); + REQUIRE(skiddingbraking.skidded); + + CHECK(std::abs(normal.speed()) < 0.1); + CHECK(std::abs(skidding.speed()) < 0.1); + CHECK_FALSE(skidding->SlippingWheels); +} diff --git a/tests/unit/vehicle.cpp b/tests/unit/vehicle.cpp index 135c5c2fb..c0930d134 100644 --- a/tests/unit/vehicle.cpp +++ b/tests/unit/vehicle.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "vehicle.h" +#include "vehicle/DynObj.h" #include #include @@ -159,6 +160,18 @@ vehicle::vehicle(definition const &Definition, double Velocity, std::string cons } void vehicle::step(track const &Track, double Dt) { + compute_forces(Dt); + compute_movement(Track, Dt, TLocation{0.0, 0.0, 0.0}); +} + +void vehicle::compute_forces(double Dt) { + if (m_pipepressure >= 0.0) { + m_mover->Pipe->CreatePress(m_pipepressure); + } + m_mover->ComputeTotalForce(Dt); +} + +double vehicle::compute_movement(track const &Track, double Dt, TLocation const &Location) { TTrackShape shape; shape.Len = 1000.0; shape.dHtrack = Track.gradient; @@ -167,13 +180,8 @@ void vehicle::step(track const &Track, double Dt) { param.friction = Track.friction; param.CategoryFlag = 1; TTractionParam const traction; - TLocation const location{0.0, 0.0, 0.0}; TRotation const rotation{0.0, 0.0, 0.0}; - if (m_pipepressure >= 0.0) { - m_mover->Pipe->CreatePress(m_pipepressure); - } - m_mover->ComputeTotalForce(Dt); - m_mover->ComputeMovement(Dt, Dt, shape, param, traction, location, rotation); + return m_mover->ComputeMovement(Dt, Dt, shape, param, traction, Location, rotation); } void vehicle::run(double Seconds, track const &Track, double Dt) { @@ -184,4 +192,88 @@ void vehicle::run(double Seconds, track const &Track, double Dt) { } } +// the physics reaches the vehicle next to it through TDynamicObject::MoverParameters only (neighbour_data). the +// tests can't make a TDynamicObject, whose constructor and members come with the rest of the engine, so the +// neighbour is a zeroed block of its size with just that pointer set: enough for the physics, which reads nothing +// else of it, and never used as a whole object +static_assert(alignof(TDynamicObject) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__); + +namespace { + +std::vector make_standin(TMoverParameters &Mover) { + std::vector standin(sizeof(TDynamicObject)); // zeroed + reinterpret_cast(standin.data())->MoverParameters = &Mover; // NOSONAR see above + return standin; +} + +TDynamicObject *as_neighbour(std::byte *Standin) { + return reinterpret_cast(Standin); // NOSONAR see above +} + +} // namespace + +pair::pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap) { + auto const &first{First.mover()}; + auto const &second{Second.mover()}; + // the first vehicle in the middle, its FirstEnd towards the second one + m_vehicles[0] = {&First, FirstEnd, 0.0, FirstEnd == end::front ? 1.0 : -1.0, make_standin(First.mover())}; + m_vehicles[1] = {&Second, SecondEnd, 0.5 * first.Dim.L + Gap + 0.5 * second.Dim.L, SecondEnd == end::front ? -1.0 : 1.0, make_standin(Second.mover())}; +} + +pair::~pair() { + for (auto const &placed : m_vehicles) { + // the vehicles outlive the pair, they mustn't point at its stand-ins + for (auto &neighbour : placed.owner->mover().Neighbours) { + neighbour = neighbour_data(); + } + } +} + +void pair::find_neighbours() { + // as TDynamicObject::update_neighbours() + for (auto const &placed : m_vehicles) { + placed.owner->mover().Loc = TLocation{placed.position, 0.0, 0.0}; + } + for (int i = 0; i < 2; ++i) { + auto const &self{m_vehicles[i]}; + auto &other{m_vehicles[1 - i]}; // its stand-in is what this one points at + auto &mover{self.owner->mover()}; + auto const &othermover{other.owner->mover()}; + auto &neighbour{mover.Neighbours[self.end]}; + neighbour.vehicle = as_neighbour(other.standin.data()); + neighbour.vehicle_end = other.end; + neighbour.distance = static_cast(TMoverParameters::CouplerDist(&mover, &othermover) - mover.Couplers[self.end].adapter_length - + othermover.Couplers[other.end].adapter_length); + mover.Neighbours[1 - self.end] = neighbour_data(); + } +} + +void pair::step(track const &Track, double Dt) { + find_neighbours(); + for (auto const &placed : m_vehicles) { + placed.owner->compute_forces(Dt); + } + for (auto &placed : m_vehicles) { + placed.position += placed.heading * placed.owner->compute_movement(Track, Dt, TLocation{placed.position, 0.0, 0.0}); + } +} + +void pair::run(double Seconds, track const &Track, double Dt) { + // whole steps, as in vehicle::run() + auto const steps{std::lround(Seconds / Dt)}; + for (long i = 0; i < steps; ++i) { + step(Track, Dt); + } +} + +double pair::gap() const { + auto const &first{m_vehicles[0].owner->mover()}; + auto const &second{m_vehicles[1].owner->mover()}; + return std::abs(m_vehicles[1].position - m_vehicles[0].position) - 0.5 * (first.Dim.L + second.Dim.L); +} + +int pair::coupling() const { + return m_vehicles[0].owner->mover().Couplers[m_vehicles[0].end].CouplingFlag; +} + } // namespace test diff --git a/tests/unit/vehicle.h b/tests/unit/vehicle.h index ad036839e..616e75d11 100644 --- a/tests/unit/vehicle.h +++ b/tests/unit/vehicle.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include @@ -64,8 +66,57 @@ public: double speed() const { return m_mover->V * 3.6; } private: + friend class pair; + + // the two halves of a step: the forces, taken by all the vehicles in the scene first, then the movement, + // the vehicle placed at Location. returns the distance travelled [m], positive forwards + void compute_forces(double Dt); + double compute_movement(track const &Track, double Dt, TLocation const &Location); + std::unique_ptr m_mover; double m_pipepressure{-1.0}; }; +// two vehicles on a straight track, the FirstEnd of the first one facing the SecondEnd of the second one Gap metres +// away (from coupler to coupler), with nothing else around. a step runs them the way the simulation runs the +// vehicles of a scene (vehicle_table::update()): each one finds its neighbour, then both take the forces, the +// coupler and buffer ones among them (CouplerForce()), then both move and handle the collisions (CollisionDetect()). +// they couple and part on their own, as the couplers let them; the speed of each runs along its own axis +// test::pair pair{moving, end::front, standing, end::rear, 1.0}; // 1 m apart +// pair.run(5.0); // for 5 s +// CHECK(pair.coupled()); +class pair { +public: + pair(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd, double Gap); + ~pair(); + pair(pair const &) = delete; + pair &operator=(pair const &) = delete; + // the vehicles point at the stand-ins of the pair, which stay where they are + pair(pair &&) = delete; + pair &operator=(pair &&) = delete; + + // advances both vehicles by one step of Dt + void step(track const &Track = {}, double Dt = 0.01); + // advances both vehicles by Seconds in steps of Dt + void run(double Seconds, track const &Track = {}, double Dt = 0.01); + // distance between the facing couplers [m], negative while the buffers are pressed in + double gap() const; + // type of the coupling between the vehicles, coupling::faux when they aren't coupled + int coupling() const; + bool coupled() const { return coupling() != coupling::faux; } + +private: + struct placement { + vehicle *owner{nullptr}; + int end{0}; // the end facing the other vehicle + double position{0.0}; // of the middle of the vehicle along the track [m] + double heading{1.0}; // +1 when the vehicle runs forwards towards the growing position, -1 otherwise + std::vector standin; // what the other vehicle sees as this one, see vehicle.cpp + }; + + void find_neighbours(); + + std::array m_vehicles; +}; + } // namespace test diff --git a/utilities/translation.cpp b/utilities/translation.cpp index b1db12275..c0dbeb1a0 100644 --- a/utilities/translation.cpp +++ b/utilities/translation.cpp @@ -218,6 +218,7 @@ std::string locale::label_cab_control(std::string const &Label) { "watercircuitslink_sw:", STRN("water circuits link") }, { "fuelpump_sw:", STRN("fuel pump") }, { "oilpump_sw:", STRN("oil pump") }, + { "oilfuelpump_sw:", STRN("oil and fuel pumps") }, { "motorblowersfront_sw:", STRN("motor blowers A") }, { "motorblowersrear_sw:", STRN("motor blowers B") }, { "motorblowersalloff_sw:", STRN("all motor blowers") }, diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 5beac4c1e..08f0c0d24 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -30,6 +30,8 @@ http://mozilla.org/MPL/2.0/. #include "world/station.h" #include "input/keyboardinput.h" +#include + #define LOGVELOCITY 0 #define LOGORDERS 1 #define LOGSTOPS 1 @@ -1093,10 +1095,9 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub if (Point.fDist < passengerstopmaxdistance * 0.5 ) { // zaliczamy posterunek w pewnej odległości przed (choć W4 nie zasłania już semafora) #if LOGSTOPS - WriteLog( - pVehicle->asName + " as " + TrainParams.TrainName - + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute) - + " passed " + asNextStop); // informacja + WriteLog(std::format( + "{} as {}: at {}:{} passed {}", + pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute, asNextStop)); // informacja #endif // przy jakim dystansie (stanie licznika) ma przesunąć na następny postój fLastStopExpDist = mvOccupied->DistCounter + 0.250 + 0.001 * fLength; @@ -1253,10 +1254,9 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub TrainParams.StationIndexInc(); // przejście do następnej asNextStop = TrainParams.NextStop(); // pobranie kolejnego miejsca zatrzymania #if LOGSTOPS - WriteLog( - pVehicle->asName + " as " + TrainParams.TrainName - + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute) - + " next " + asNextStop); // informacja + WriteLog(std::format( + "{} as {}: at {}:{} next {}", + pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute, asNextStop)); // informacja #endif // update consist weight, brake settings and ai braking tables // NOTE: this calculation is expected to run after completing loading/unloading @@ -1297,13 +1297,16 @@ TController::TableUpdateStopPoint( TCommandType &Command, TSpeedPos &Point, doub else { // jeśli dojechaliśmy do końca rozkładu #if LOGSTOPS - WriteLog( - pVehicle->asName + " as " + TrainParams.TrainName - + ": at " + std::to_string(simulation::Time.data().wHour) + ":" + std::to_string(simulation::Time.data().wMinute) - + " end of route."); // informacja + WriteLog(std::format( + "{} as {}: at {}:{} end of route.", + pVehicle->asName, TrainParams.TrainName, simulation::Time.data().wHour, simulation::Time.data().wMinute)); // informacja #endif asNextStop = TrainParams.NextStop(); // informacja o końcu trasy TrainParams.NewName("none"); // czyszczenie nieaktualnego rozkładu + // koniec kursu, gasimy wyświetlacze relacji (tablice blaszane zostają) + for( auto *vehicle { pVehicles[ end::front ] }; vehicle != nullptr; vehicle = vehicle->Next() ) { + vehicle->DestinationClear(); + } // ma nie podjeżdżać pod W4 i ma je pomijać iDrivigFlags &= ~moveStopCloser; if( false == TestFlag( iDrivigFlags, movePushPull ) ) { diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 32ba307f7..c6793e454 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -4150,6 +4150,8 @@ bool TDynamicObject::Update(double dt, double dt1) // mirrors + auto const mirrorleft { dMirrorMoveL }; + auto const mirrorright { dMirrorMoveR }; if( MoverParameters->Vel > MoverParameters->MirrorVelClose || MoverParameters->CabActive == 0 && activation::mirrors || MoverParameters->MirrorForbidden ) { @@ -4180,6 +4182,8 @@ bool TDynamicObject::Update(double dt, double dt1) dMirrorMoveR + 1.0 * dt1 ); } } + play_mirror_sounds( side::left, m_mirrormotion[ side::left ].update( dMirrorMoveL - mirrorleft ) ); + play_mirror_sounds( side::right, m_mirrormotion[ side::right ].update( dMirrorMoveR - mirrorright ) ); // compartment lights // if the vehicle has a controller, we base the light state on state of the controller otherwise we check the vehicle itself @@ -4825,12 +4829,13 @@ void TDynamicObject::RenderSounds() { // horns { - // for moving vehicle combine regular horn activation flag with emergency brake horn activation flag, if the brake is active - auto const warningsignal { ( - MoverParameters->Vel > 0.5 && MoverParameters->AlarmChainFlag ? + // for moving vehicle combine regular horn activation flag with emergency brake horn activation flag, if the brake is active; + // the horns without the air or the voltage they need stay silent + auto const warningsignal { MoverParameters->SoundingHorns( + ( MoverParameters->Vel > 0.5 && MoverParameters->AlarmChainFlag ? MoverParameters->EmergencyBrakeWarningSignal : 0 ) - | MoverParameters->WarningSignal }; + | MoverParameters->WarningSignal ) }; if( TestFlag( warningsignal, 1 ) ) { sHorn1.play( sound_flags::exclusive | sound_flags::looping ); @@ -6363,6 +6368,19 @@ void TDynamicObject::LoadMMediaFile( std::string const &TypeName, std::string co } } + else if( token == "mirroropen:" || token == "mirrorclose:" ) { + sound_source soundtemplate { sound_placement::general }; + soundtemplate.deserialize( parser, sound_type::single ); + soundtemplate.owner( this ); + for( auto &mirror : m_mirrorsounds ) { + // apply configuration to all defined mirrors, but preserve their individual offsets + auto &sound { token == "mirroropen:" ? mirror.unfold : mirror.fold }; + auto const mirroroffset { sound.offset() }; + sound = soundtemplate; + sound.offset( mirroroffset ); + } + } + else if( token == "doorstepclose:" ) { sound_source soundtemplate { sound_placement::general }; soundtemplate.deserialize( parser, sound_type::single ); @@ -6576,6 +6594,27 @@ void TDynamicObject::LoadMMediaFile( std::string const &TypeName, std::string co } } + else if( token == "mirrors:" ) { + // a list of offsets along vehicle's z-axis of the mirror pairs, one at each cab; followed with "end" + while( (token = parser.getToken()) != "" + && token != "end" ) { + // vehicle faces +Z in 'its' space, as for the doors negative value means ahead of centre + auto const offset { std::atof( token.c_str() ) * -1.f }; + mirror_sounds mirror; + mirror.end = offset >= 0.f ? end::front : end::rear; + for( auto const mirrorside : { side::left, side::right } ) { + auto const location { glm::vec3 { + MoverParameters->Dim.W * ( mirrorside == side::left ? 0.5f : -0.5f ), + MoverParameters->Dim.H * 0.5f, + offset } }; + mirror.placement = mirrorside; + mirror.unfold.offset( location ); + mirror.fold.offset( location ); + m_mirrorsounds.emplace_back( mirror ); + } + } + } + else if( token == "tractionmotors:" ) { // a list of offsets along vehicle's z-axis; followed with "end" while( (token = parser.getToken()) != "" @@ -7794,7 +7833,29 @@ int TDynamicObject::RouteWish(TTrack *tr) return Mechanik ? Mechanik->CrossRoute(tr) : 0; // wg AI albo prosto }; -void TDynamicObject::DestinationSet(std::string to, std::string numer) { +void TDynamicObject::play_mirror_sounds( side const Side, mirror_moves::event const Event ) { + + if( Event == mirror_moves::event::none ) { return; } + // as UpdateMirror(), only the mirrors of the occupied cab move + if( MoverParameters->CabOccupied == 0 ) { return; } + auto const cabend { MoverParameters->CabOccupied > 0 ? end::front : end::rear }; + for( auto &mirror : m_mirrorsounds ) { + if( mirror.placement != Side || mirror.end != cabend ) { continue; } + auto &sound { Event == mirror_moves::event::unfolding ? mirror.unfold : mirror.fold }; + auto &other { Event == mirror_moves::event::unfolding ? mirror.fold : mirror.unfold }; + other.stop(); + sound.play( sound_flags::exclusive ); + } +} + +void TDynamicObject::DestinationClear() { + + if( false == DestinationSign.has_light ) { return; } // physical destination signs remain up until manually changed + // update_destinations() shows the blank sign in place of the missing destination + DestinationSign.destination = null_handle; +} + +void TDynamicObject::DestinationSet(std::string const &to, std::string const &numer) { // ustawienie stacji docelowej oraz wymiennej tekstury 4, jeśli istnieje plik // w zasadzie, to każdy wagon mógłby mieć inną stację docelową // zwłaszcza w towarowych, pod kątem zautomatyzowania maewrów albo pracy górki @@ -7823,6 +7884,15 @@ void TDynamicObject::DestinationSet(std::string to, std::string numer) { if( DestinationSign.script.empty() ) { return; } // no script so no way to make the texture if( numer == "none" ) { return; } // blank or incomplete/malformed timetable, don't bother + auto const trainname { [this]() -> std::string { + if( ctOwner != nullptr ) { return ctOwner->TrainName(); } + if( Mechanik != nullptr ) { return Mechanik->TrainName(); } + return "none"; } }; // shouldn't get here but, eh + auto const instance { [this]() -> std::string { + if( DestinationSign.instancing == "name" ) { return MoverParameters->Name; } + if( DestinationSign.instancing == "type" ) { return MoverParameters->TypeName; } + return "none"; } }; + std::string signrequest { "make:" + DestinationSign.script @@ -7834,17 +7904,10 @@ void TDynamicObject::DestinationSet(std::string to, std::string numer) { ctOwner->Vehicle()->MoverParameters->Name ) // owned vehicle, safer to point to owner as carriages can have identical names // basic instancing string // NOTE: underscore doesn't have any magic meaning for the time being, it's just less likely to conflict with regular dictionary keys - + "&_id1=" + ( - ctOwner != nullptr ? ctOwner->TrainName() : - Mechanik != nullptr ? Mechanik->TrainName() : - "none" ) }; // shouldn't get here but, eh + + "&_id1=" + trainname() }; // TBD, TODO: replace instancing with support for variables in extra parameters string? if( false == DestinationSign.instancing.empty() ) { - signrequest += - "&_id2=" + ( - DestinationSign.instancing == "name" ? MoverParameters->Name : - DestinationSign.instancing == "type" ? MoverParameters->TypeName : - "none" ); + signrequest += "&_id2=" + instance(); } // optionl extra parameters if( false == DestinationSign.parameters.empty() ) { diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 36335a320..2d9636b2f 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "interfaces/ITexture.h" #include "audio/sound.h" #include "world/Spring.h" +#include "vehicle/mirrors.h" #include @@ -393,6 +394,13 @@ private: side placement {}; }; + struct mirror_sounds { + sound_source unfold { sound_placement::general }; + sound_source fold { sound_placement::general }; + side placement {}; + int end {}; // of the vehicle, the mirror moves with the cab at this end occupied + }; + struct exchange_sounds { sound_source loading { sound_placement::general }; sound_source unloading { sound_placement::general }; @@ -567,6 +575,8 @@ private: std::vector m_pantographsounds; // typically 2 but can be less (or more?) std::vector m_doorsounds; // can expect symmetrical arrangement, but don't count on it bool m_doorlocks { false }; // sound helper, current state of door locks + std::vector m_mirrorsounds; + std::array m_mirrormotion; // indexed by side: right, left sound_source sHorn1 { sound_placement::external, 5 * EU07_SOUND_RUNNINGNOISECUTOFFRANGE }; sound_source sHorn2 { sound_placement::external, 5 * EU07_SOUND_RUNNINGNOISECUTOFFRANGE }; sound_source sHorn3 { sound_placement::external, 5 * EU07_SOUND_RUNNINGNOISECUTOFFRANGE }; @@ -703,6 +713,8 @@ private: void Move(double fDistance); void FastMove(double fDistance); void RenderSounds(); + // plays the sounds of the mirrors on the Side, of the occupied cab, starting the Event + void play_mirror_sounds( side const Side, mirror_moves::event const Event ); inline glm::dvec3 GetPosition() const { return vPosition; }; // converts location from vehicle coordinates frame to world frame @@ -815,7 +827,9 @@ private: void ParamSet(int what, int into); // zapytanie do AI, po którym segmencie skrzyżowania jechać int RouteWish(TTrack *tr); - void DestinationSet(std::string to, std::string numer); + void DestinationSet(std::string const &to, std::string const &numer); + // blanks electronic destination sign at the end of the service; physical signs stay until changed + void DestinationClear(); material_handle DestinationFind( std::string Destination ); void OverheadTrack(float o); glm::dvec3 get_future_movement() const; diff --git a/vehicle/Gauge.cpp b/vehicle/Gauge.cpp index d7a41062e..af8e9a060 100644 --- a/vehicle/Gauge.cpp +++ b/vehicle/Gauge.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "vehicle/Gauge.h" +#include "vehicle/gaugepositions.h" #include "utilities/parser.h" #include "model/Model3d.h" #include "vehicle/DynObj.h" @@ -302,42 +303,37 @@ bool TGauge::UpdateValue( float fNewDesired ) { auto const desiredtimes100 = static_cast( std::round( 100.0 * fNewDesired ) ); - if( desiredtimes100 == static_cast( std::round( 100.0 * m_targetvalue ) ) ) { + auto const previoustimes100 = static_cast( std::round( 100.0 * m_targetvalue ) ); + if( desiredtimes100 == previoustimes100 ) { return true; } m_targetvalue = fNewDesired; - // if there's any sound associated with new requested value, play it - // check value-specific table first... - if( auto const fullinteger { desiredtimes100 % 100 == 0 }; fullinteger ) { - // filter out values other than full integers - auto const lookup = m_soundfxvalues.find( desiredtimes100 / 100 ); - if( lookup != m_soundfxvalues.end() ) { - lookup->second.play(); - return true; - } - } - else { + if( desiredtimes100 % 100 != 0 ) { // toggle the control to continous range/exclusive sound mode from now on m_soundtype = sound_flags::exclusive; } + // if there's any sound associated with a position reached on the way to the new requested value, play it + // check value-specific table first... + if( auto const reached = gauge_positions::last_reached( m_soundfxvalues, previoustimes100, desiredtimes100 ); + reached != m_soundfxvalues.end() ) { + reached->second.play(); + return true; + } // ...and if there isn't any, fall back on the basic set... // HACK: crude way to discern controls with continuous and quantized value range - if( auto const currentvalue = GetValue(); currentvalue < fNewDesired ) { + auto const currentvalue = GetValue(); + if( currentvalue < fNewDesired && false == m_soundfxincrease.empty() ) { // shift up - if( false == m_soundfxincrease.empty() ) { - m_soundfxincrease.play( m_soundtype ); - return true; - } + m_soundfxincrease.play( m_soundtype ); + return true; } - else if( currentvalue > fNewDesired ) { + if( currentvalue > fNewDesired && false == m_soundfxdecrease.empty() ) { // shift down - if( false == m_soundfxdecrease.empty() ) { - m_soundfxdecrease.play( m_soundtype ); - return true; - } + m_soundfxdecrease.play( m_soundtype ); + return true; } return false; // no suitable sound was found -}; +} void TGauge::PutValue(float fNewDesired) { // McZapkie-281102: natychmiastowe wpisanie wartosci diff --git a/vehicle/Train.cpp b/vehicle/Train.cpp index f0b2367f7..201b8972f 100644 --- a/vehicle/Train.cpp +++ b/vehicle/Train.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "vehicle/Train.h" +#include "vehicle/horn.h" #include "utilities/Globals.h" #include "simulation/simulation.h" @@ -340,6 +341,7 @@ TTrain::commandhandler_map const TTrain::m_commandhandlers = { {user_command::fuelpumpenable, &TTrain::OnCommand_fuelpumpenable}, {user_command::fuelpumpdisable, &TTrain::OnCommand_fuelpumpdisable}, {user_command::oilpumptoggle, &TTrain::OnCommand_oilpumptoggle}, + {user_command::oilfuelpumptoggle, &TTrain::OnCommand_oilfuelpumptoggle}, {user_command::oilpumpenable, &TTrain::OnCommand_oilpumpenable}, {user_command::oilpumpdisable, &TTrain::OnCommand_oilpumpdisable}, {user_command::waterheaterbreakertoggle, &TTrain::OnCommand_waterheaterbreakertoggle}, @@ -3995,6 +3997,37 @@ void TTrain::OnCommand_oilpumpdisable(TTrain *Train, command_data const &Command } } +void TTrain::OnCommand_oilfuelpumptoggle(TTrain *Train, command_data const &Command) +{ + if (Command.action == GLFW_REPEAT) + { + return; + } + + auto *vehicle{Train->mvControlled}; + if (Train->ggOilFuelPumpButton.type() == TGaugeType::push) + { + // impulse switch, as the separate ones: held to start both pumps + auto const pressed{Command.action == GLFW_PRESS}; + Train->ggOilFuelPumpButton.UpdateValue(pressed ? 1.0 : 0.0, Train->dsbSwitch); + vehicle->FuelPumpSwitch(pressed); + vehicle->OilPumpSwitch(pressed); + return; + } + // two-state switch, only cares about press events + if (Command.action != GLFW_PRESS) + { + return; + } + // both pumps go off if both were on, otherwise both go on; a pump started automatically doesn't take part + auto const enable{false == vehicle->OilFuelPumpsEnabled()}; + Train->ggOilFuelPumpButton.UpdateValue(enable ? 1.0 : 0.0, Train->dsbSwitch); + vehicle->FuelPumpSwitch(enable); + vehicle->FuelPumpSwitchOff(false == enable); + vehicle->OilPumpSwitch(enable); + vehicle->OilPumpSwitchOff(false == enable); +} + void TTrain::OnCommand_waterheaterbreakertoggle(TTrain *Train, command_data const &Command) { @@ -6605,7 +6638,7 @@ void TTrain::OnCommand_heatingtoggle(TTrain *Train, command_data const &Command) OnCommand_heatingdisable(Train, Command); } } - else if (Command.action == GLFW_RELEASE && Train->ggTrainHeatingButton.type() == TGaugeType::push) + else if (Command.action == GLFW_RELEASE && (Train->ggTrainHeatingButton.type() == TGaugeType::push || Train->mvOccupied->HeatingSwitchImpulse)) { // impulse switch // visual feedback @@ -6623,6 +6656,11 @@ void TTrain::OnCommand_heatingenable(TTrain *Train, command_data const &Command) // visual feedback Train->ggTrainHeatingButton.UpdateValue(1.0, Train->dsbSwitch); } + else if (Command.action == GLFW_RELEASE && Train->mvOccupied->HeatingSwitchImpulse) + { + // the switch springs back to the middle + Train->ggTrainHeatingButton.UpdateValue(0.0, Train->dsbSwitch); + } } void TTrain::OnCommand_heatingdisable(TTrain *Train, command_data const &Command) @@ -6632,8 +6670,21 @@ void TTrain::OnCommand_heatingdisable(TTrain *Train, command_data const &Command { Train->mvOccupied->HeatingSwitch(false); - // visual feedback - Train->ggTrainHeatingButton.UpdateValue(Train->ggTrainHeatingButton.type() == TGaugeType::push ? 1.0 : 0.0, Train->dsbSwitch); + // visual feedback: an impulse switch is pushed the other way, down + auto position{0.0}; + if (Train->mvOccupied->HeatingSwitchImpulse) + { + position = -1.0; + } + else if (Train->ggTrainHeatingButton.type() == TGaugeType::push) + { + position = 1.0; + } + Train->ggTrainHeatingButton.UpdateValue(position, Train->dsbSwitch); + } + else if (Command.action == GLFW_RELEASE && Train->mvOccupied->HeatingSwitchImpulse) + { + Train->ggTrainHeatingButton.UpdateValue(0.0, Train->dsbSwitch); } } @@ -7841,16 +7892,10 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman if (Command.action == GLFW_PRESS) { // only need to react to press, sound will continue until stopped - if (false == TestFlag(Train->mvOccupied->WarningSignal, 1)) + if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::low)) { // turn on - Train->mvOccupied->WarningSignal |= 1; - /* - if( true == TestFlag( Train->mvOccupied->WarningSignal, 2 ) ) { - // low and high horn are treated as mutually exclusive - Train->mvControlled->WarningSignal &= ~2; - } - */ + Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::low, Train->has_single_horn_lever()); // visual feedback Train->ggHornButton.UpdateValue(-1.0); Train->ggHornLowButton.UpdateValue(1.0); @@ -7859,13 +7904,13 @@ void TTrain::OnCommand_hornlowactivate(TTrain *Train, command_data const &Comman else if (Command.action == GLFW_RELEASE) { // turn off - /* - // NOTE: we turn off both low and high horn, due to unreliability of release event when shift key is involved - Train->mvOccupied->WarningSignal &= ~( 1 | 2 ); - */ - Train->mvOccupied->WarningSignal &= ~1; + Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::low); // visual feedback - Train->ggHornButton.UpdateValue(0.0); + if (false == (Train->has_single_horn_lever() && TestFlag(Train->mvOccupied->WarningSignal, horn::high))) + { + // leave the single lever alone if it was moved to the other tone in the meantime + Train->ggHornButton.UpdateValue(0.0); + } Train->ggHornLowButton.UpdateValue(0.0); } } @@ -7885,16 +7930,10 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma if (Command.action == GLFW_PRESS) { // only need to react to press, sound will continue until stopped - if (false == TestFlag(Train->mvOccupied->WarningSignal, 2)) + if (false == TestFlag(Train->mvOccupied->WarningSignal, horn::high)) { // turn on - Train->mvOccupied->WarningSignal |= 2; - /* - if( true == TestFlag( Train->mvOccupied->WarningSignal, 1 ) ) { - // low and high horn are treated as mutually exclusive - Train->mvControlled->WarningSignal &= ~1; - } - */ + Train->mvOccupied->WarningSignal = horn::pressed(Train->mvOccupied->WarningSignal, horn::high, Train->has_single_horn_lever()); // visual feedback Train->ggHornButton.UpdateValue(1.0); Train->ggHornHighButton.UpdateValue(1.0); @@ -7903,13 +7942,13 @@ void TTrain::OnCommand_hornhighactivate(TTrain *Train, command_data const &Comma else if (Command.action == GLFW_RELEASE) { // turn off - /* - // NOTE: we turn off both low and high horn, due to unreliability of release event when shift key is involved - Train->mvOccupied->WarningSignal &= ~( 1 | 2 ); - */ - Train->mvOccupied->WarningSignal &= ~2; + Train->mvOccupied->WarningSignal = horn::released(Train->mvOccupied->WarningSignal, horn::high); // visual feedback - Train->ggHornButton.UpdateValue(0.0); + if (false == (Train->has_single_horn_lever() && TestFlag(Train->mvOccupied->WarningSignal, horn::low))) + { + // leave the single lever alone if it was moved to the other tone in the meantime + Train->ggHornButton.UpdateValue(0.0); + } Train->ggHornHighButton.UpdateValue(0.0); } } @@ -7953,12 +7992,7 @@ void TTrain::OnCommand_radiotoggle(TTrain *Train, command_data const &Command) return; } - // NOTE: we ignore the lack of 3d model to allow system reset after receiving radio-stop signal - /* - if( false == Train->m_controlmapper.contains( "radio_sw:" ) ) { - return; - } - */ + // NOTE: works without the switch in the 3d model too, to allow system reset after receiving radio-stop signal // only reacting to press, so the sound can loop uninterrupted if (false == Train->mvOccupied->Radio) { @@ -8003,9 +8037,13 @@ void TTrain::OnCommand_radiochannelincrease(TTrain *Train, command_data const &C if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->RadioChannel() + 1; - OnCommand_radiochannelset(Train, newCommand); + // the channel buttons of a radio switched off or without power do nothing + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->RadioChannel() + 1; + OnCommand_radiochannelset(Train, newCommand); + } Train->ggRadioChannelNext.UpdateValue(1.0); } @@ -8021,9 +8059,12 @@ void TTrain::OnCommand_radiochanneldecrease(TTrain *Train, command_data const &C if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->RadioChannel() - 1; - OnCommand_radiochannelset(Train, newCommand); + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->RadioChannel() - 1; + OnCommand_radiochannelset(Train, newCommand); + } Train->ggRadioChannelPrevious.UpdateValue(1.0); } @@ -8049,7 +8090,7 @@ void TTrain::OnCommand_radiostopsend(TTrain *Train, command_data const &Command) if (Command.action == GLFW_PRESS) { - if (true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->mvOccupied->RadioPowered()) { simulation::Region->RadioStop(Train->Dynamic()->GetPosition()); } @@ -8067,7 +8108,7 @@ void TTrain::OnCommand_radiostopenable(TTrain *Train, command_data const &Comman { if (Command.action == GLFW_PRESS && Train->ggRadioStop.GetValue() == 0) { - if (true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->mvOccupied->RadioPowered()) { simulation::Region->RadioStop(Train->Dynamic()->GetPosition()); } @@ -8090,7 +8131,7 @@ void TTrain::OnCommand_radiostoptest(TTrain *Train, command_data const &Command) if (Command.action == GLFW_PRESS) { - if (Train->RadioChannel() == 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->RadioChannel() == 10 && Train->mvOccupied->RadioPowered()) { Train->Dynamic()->RadioStop(); } @@ -8108,7 +8149,7 @@ void TTrain::OnCommand_radiocall1send(TTrain *Train, command_data const &Command { if (Command.action == GLFW_PRESS) { - if (Train->RadioChannel() != 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->RadioChannel() != 10 && Train->mvOccupied->RadioPowered()) { simulation::Events.queue_receivers(radio_message::call1, Train->Dynamic()->GetPosition()); } @@ -8127,7 +8168,7 @@ void TTrain::OnCommand_radiocall3send(TTrain *Train, command_data const &Command if (Command.action == GLFW_PRESS) { - if (Train->RadioChannel() != 10 && true == Train->mvOccupied->Radio && (Train->mvOccupied->Power24vIsAvailable || Train->mvOccupied->Power110vIsAvailable)) + if (Train->RadioChannel() != 10 && Train->mvOccupied->RadioPowered()) { simulation::Events.queue_receivers(radio_message::call3, Train->Dynamic()->GetPosition()); } @@ -8146,9 +8187,13 @@ void TTrain::OnCommand_radiovolumeincrease(TTrain *Train, command_data const &Co if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->m_radiovolume + 0.125; - OnCommand_radiovolumeset(Train, newCommand); + // so do the volume buttons + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->m_radiovolume + 0.125; + OnCommand_radiovolumeset(Train, newCommand); + } Train->ggRadioVolumeNext.UpdateValue(1.0); } else if (Command.action == GLFW_RELEASE) @@ -8163,9 +8208,12 @@ void TTrain::OnCommand_radiovolumedecrease(TTrain *Train, command_data const &Co if (Command.action == GLFW_PRESS) { - command_data newCommand = Command; - newCommand.param1 = Train->m_radiovolume - 0.125; - OnCommand_radiovolumeset(Train, newCommand); + if (Train->mvOccupied->RadioPowered()) + { + command_data newCommand = Command; + newCommand.param1 = Train->m_radiovolume - 0.125; + OnCommand_radiovolumeset(Train, newCommand); + } Train->ggRadioVolumePrevious.UpdateValue(1.0); } else if (Command.action == GLFW_RELEASE) @@ -8931,9 +8979,9 @@ bool TTrain::Update(double const Deltatime) btLampkaOpory.Turn(mvControlled->StLinFlag && mvControlled->ResistorsFlagCheck()); btLampkaBezoporowa.Turn(mvControlled->ResistorsFlagCheck() || mvControlled->MainCtrlActualPos == 0); // do EU04 - btLampkaStyczn.Turn(!mvControlled->StLinFlag && !mvControlled->ControlPressureSwitch && mvControlled->BrakePress < 1.0); // mozna prowadzic rozruch - btLampkaPrzekRozn.Turn(!mvControlled->GroundRelay && !mvControlled->ControlPressureSwitch && mvControlled->BrakePress < 1.0); // relay is off and needs a reset - btLampkaNadmSil.Turn(mvControlled->FuseFlagCheck() && !mvControlled->ControlPressureSwitch && mvControlled->BrakePress < 1.0); // relay is off and needs a reset + btLampkaStyczn.Turn(mvControlled->LineContactorsLampLit()); // mozna prowadzic rozruch + btLampkaPrzekRozn.Turn(!mvControlled->GroundRelay && mvControlled->MainCircuitLampsEnabled()); // relay is off and needs a reset + btLampkaNadmSil.Turn(mvControlled->FuseFlagCheck() && mvControlled->MainCircuitLampsEnabled()); // relay is off and needs a reset btLampkaUkrotnienie.Turn(TestFlag(mvControlled->Couplers[mvControlled->CabOccupied == 1 ? rear : front].CouplingFlag, control)); btLampkaHamPosp.Turn(TestFlag(mvOccupied->Hamulec->GetBrakeStatus(), 1)); // lampka drugiego stopnia hamowania // TODO: youBy wyciągnąć flagę wysokiego stopnia @@ -9180,14 +9228,7 @@ bool TTrain::Update(double const Deltatime) btLampkaOporyB.Turn(mover->ResistorsFlagCheck()); btLampkaBezoporowaB.Turn(true == mover->ResistorsFlagCheck() || mover->MainCtrlActualPos == 0); // do EU04 - if (mover->StLinFlag || mover->ControlPressureSwitch) - { - btLampkaStycznB.Turn(false); - } - else if (mover->BrakePress < 1.0) - { - btLampkaStycznB.Turn(true); // mozna prowadzic rozruch - } + btLampkaStycznB.Turn(mover->LineContactorsLampLit()); // mozna prowadzic rozruch // hunter-271211: sygnalizacja poslizgu w pierwszym pojezdzie, gdy wystapi w drugim if (mover->SlippingWheels) { @@ -9583,6 +9624,7 @@ bool TTrain::Update(double const Deltatime) ggWaterCircuitsLinkButton.Update(); ggFuelPumpButton.Update(); ggOilPumpButton.Update(); + ggOilFuelPumpButton.Update(); ggMotorBlowersFrontButton.Update(); ggMotorBlowersRearButton.Update(); ggMotorBlowersAllOffButton.Update(); @@ -10106,7 +10148,7 @@ void TTrain::update_sounds_radio() std::erase_if(m_radiomessages, [](auto const &source) { return false == source.second->is_playing(); }); } // adjust audibility of remaining messages based on current radio conditions - auto const radioenabled{true == mvOccupied->Radio && (mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable)}; + auto const radioenabled{mvOccupied->RadioPowered()}; for (auto const &[messagechannel, messagesound] : m_radiomessages) { auto const volume{true == radioenabled && Dynamic()->Mechanik != nullptr && messagechannel == RadioChannel() ? m_radiovolume : 0.0}; @@ -10893,7 +10935,7 @@ void TTrain::radio_message(sound_source *Message, int const Channel) m_radiomessages.emplace_back(Channel, std::make_shared(m_radiosound)); // assign sound to the template and play it auto &message = *m_radiomessages.back().second.get(); - auto const radioenabled{true == mvOccupied->Radio && (mvOccupied->Power24vIsAvailable || mvOccupied->Power110vIsAvailable)}; + auto const radioenabled{mvOccupied->RadioPowered()}; auto const volume{true == radioenabled && Dynamic()->Mechanik != nullptr && Channel == RadioChannel() ? 1.0 : 0.0}; message.copy_sounds(*Message).gain(static_cast(volume)).play(); } @@ -11081,6 +11123,7 @@ void TTrain::clear_cab_controls() ggWaterCircuitsLinkButton.Clear(); ggFuelPumpButton.Clear(); ggOilPumpButton.Clear(); + ggOilFuelPumpButton.Clear(); ggMotorBlowersFrontButton.Clear(); ggMotorBlowersRearButton.Clear(); ggMotorBlowersAllOffButton.Clear(); @@ -11362,7 +11405,7 @@ void TTrain::set_cab_controls(int const Cab) ggDoorStepButton.PutValue(mvOccupied->Doors.step_enabled ? 1.f : 0.f); } // heating - if (false == ggTrainHeatingButton.is_push()) + if (false == ggTrainHeatingButton.is_push() && false == mvOccupied->HeatingSwitchImpulse) { ggTrainHeatingButton.PutValue(mvControlled->Heating ? 1.f : 0.f); } @@ -11415,6 +11458,11 @@ void TTrain::set_cab_controls(int const Cab) { ggOilPumpButton.PutValue(mvControlled->OilPump.is_enabled ? 1.f : 0.f); } + // joint oil and fuel pump + if (ggOilFuelPumpButton.type() != TGaugeType::push) + { + ggOilFuelPumpButton.PutValue(mvControlled->OilFuelPumpsEnabled() ? 1.f : 0.f); + } // traction motor fans if (ggMotorBlowersFrontButton.type() != TGaugeType::push) { @@ -11742,6 +11790,7 @@ bool TTrain::initialize_gauge(cParser &Parser, std::string const &Label, int con {"watercircuitslink_sw:", ggWaterCircuitsLinkButton}, {"fuelpump_sw:", ggFuelPumpButton}, {"oilpump_sw:", ggOilPumpButton}, + {"oilfuelpump_sw:", ggOilFuelPumpButton}, {"oilpressb:", ggOilPressB}, {"motorblowersfront_sw:", ggMotorBlowersFrontButton}, {"motorblowersrear_sw:", ggMotorBlowersRearButton}, diff --git a/vehicle/Train.h b/vehicle/Train.h index 4fc11dc2c..009b349b4 100644 --- a/vehicle/Train.h +++ b/vehicle/Train.h @@ -195,6 +195,9 @@ class TTrain { bool initialize_button(cParser &Parser, std::string const &Label, int const Cabindex); // helper, returns true for EMU with oerlikon brake bool is_eztoer() const; + // helper, returns true if both horn tones are operated with the same lever, without separate low and high tone buttons + bool has_single_horn_lever() const { + return ggHornButton.SubModel != nullptr && ggHornLowButton.SubModel == nullptr && ggHornHighButton.SubModel == nullptr; } // locates nearest vehicle belonging to the consist TDynamicObject *find_nearest_consist_vehicle(bool freefly, glm::vec3 pos) const; // mover master controller to specified position @@ -339,6 +342,7 @@ class TTrain { static void OnCommand_fuelpumpenable( TTrain *Train, command_data const &Command ); static void OnCommand_fuelpumpdisable( TTrain *Train, command_data const &Command ); static void OnCommand_oilpumptoggle( TTrain *Train, command_data const &Command ); + static void OnCommand_oilfuelpumptoggle( TTrain *Train, command_data const &Command ); static void OnCommand_oilpumpenable( TTrain *Train, command_data const &Command ); static void OnCommand_oilpumpdisable( TTrain *Train, command_data const &Command ); static void OnCommand_waterheaterbreakertoggle( TTrain *Train, command_data const &Command ); @@ -692,7 +696,8 @@ public: // reszta może by?publiczna TGauge ggWaterHeaterButton; // water heater switch TGauge ggWaterCircuitsLinkButton; TGauge ggFuelPumpButton; // fuel pump switch - TGauge ggOilPumpButton; // fuel pump switch + TGauge ggOilPumpButton; // oil pump switch + TGauge ggOilFuelPumpButton; // joint oil and fuel pump switch TGauge ggMotorBlowersFrontButton; // front traction motor fan switch TGauge ggMotorBlowersRearButton; // rear traction motor fan switch TGauge ggMotorBlowersAllOffButton; // motor fans shutdown switch diff --git a/vehicle/gaugepositions.h b/vehicle/gaugepositions.h new file mode 100644 index 000000000..fc82ed44b --- /dev/null +++ b/vehicle/gaugepositions.h @@ -0,0 +1,35 @@ +/* +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 + +// positions of a cab control which have sounds of their own +namespace gauge_positions { + +// the entry of the last position of Positions the control reached on its way from the Previous to the Desired value, +// both in hundredths of a position, or Positions.end() if it reached none: a lever moved smoothly passes the +// positions rather than stops at them, and should sound each one it gets to +template +typename std::map::iterator last_reached(std::map &Positions, int const Previous, int const Desired) { + auto reached{Positions.end()}; + for (auto position{Positions.begin()}; position != Positions.end(); ++position) { + auto const at{position->first * 100}; + if (Desired > Previous && at > Previous && at <= Desired) { + reached = position; // moving up, the last one in the ascending order is the one reached last + } + else if (Desired < Previous && at < Previous && at >= Desired) { + return position; // moving down, the first one is + } + } + return reached; +} + +} // namespace gauge_positions diff --git a/vehicle/horn.cpp b/vehicle/horn.cpp new file mode 100644 index 000000000..006274bb9 --- /dev/null +++ b/vehicle/horn.cpp @@ -0,0 +1,24 @@ +/* +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 "vehicle/horn.h" + +namespace horn { + +int pressed(int const Signal, int const Tone, bool const Singlelever) { + auto const other{Tone == low ? high : low}; + return (Signal | Tone) & (Singlelever ? ~other : ~0); +} + +int released(int const Signal, int const Tone) { + return Signal & ~Tone; +} + +} // namespace horn diff --git a/vehicle/horn.h b/vehicle/horn.h new file mode 100644 index 000000000..615db2f1b --- /dev/null +++ b/vehicle/horn.h @@ -0,0 +1,24 @@ +/* +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 + +// the horn controls of the cab, on the WarningSignal bits of the vehicle (1: low tone, 2: high tone) +namespace horn { + +int const low{1}; +int const high{2}; + +// the signal after the key or button of the Tone is pressed. a single lever for both tones, without +// separate buttons for them, can't sound both tones at once +int pressed(int Signal, int Tone, bool Singlelever); +// the signal after the key or button of the Tone is released +int released(int Signal, int Tone); + +} // namespace horn diff --git a/vehicle/mirrors.h b/vehicle/mirrors.h new file mode 100644 index 000000000..cf3735541 --- /dev/null +++ b/vehicle/mirrors.h @@ -0,0 +1,44 @@ +/* +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 + +// the mirrors of a vehicle, unfolded on the side of the open doors and folded at speed +namespace mirror_moves { + +enum class event { + none, + unfolding, + folding +}; + +// the move of a mirror from one update to the next. a mirror sounds as it sets off, unfolding or folding, not on +// every update it keeps moving the same way +struct motion { + int direction{0}; // of the last update: +1 unfolding, -1 folding, 0 standing + + // the event the Change of the mirror position (0: folded, 1: unfolded) since the last update starts, if any + event update(double const Change) { + auto now{0}; + if (Change > 0.0) { + now = 1; + } + else if (Change < 0.0) { + now = -1; + } + auto const setsoff{now != 0 && now != direction}; + direction = now; + if (false == setsoff) { + return event::none; + } + return now > 0 ? event::unfolding : event::folding; + } +}; + +} // namespace mirror_moves diff --git a/vr/openvr_imp.cpp b/vr/openvr_imp.cpp index 088fd5b58..15467d006 100644 --- a/vr/openvr_imp.cpp +++ b/vr/openvr_imp.cpp @@ -474,6 +474,9 @@ std::unordered_map vr_openvr::m_buttonb { "oilpump_sw:", { user_command::oilpumptoggle, user_command::none } }, + { "oilfuelpump_sw:", { + user_command::oilfuelpumptoggle, + user_command::none } }, { "motorblowersfront_sw:", { user_command::motorblowerstogglefront, user_command::none } }, From 823807201c7b1c8512fff2c45aaf23dbf062ab9e Mon Sep 17 00:00:00 2001 From: Sebastian Krzyszkowiak Date: Sun, 1 Nov 2026 13:40:57 +0100 Subject: [PATCH 06/32] Fix driver UI "mode windows" menu (#226) Make the keyboard shortcuts match the actually used ones and check the correct panels for is_open. --- application/driveruilayer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/driveruilayer.cpp b/application/driveruilayer.cpp index 569ade337..0b7b0dc46 100644 --- a/application/driveruilayer.cpp +++ b/application/driveruilayer.cpp @@ -74,9 +74,9 @@ void driver_ui::render_menu_contents() if (ImGui::BeginMenu(STR_C("Mode windows"))) { ImGui::MenuItem(m_aidpanel.title.c_str(), "F1", &m_aidpanel.is_open); - ImGui::MenuItem(m_scenariopanel.title.c_str(), "F1", &m_aidpanel.is_open); ImGui::MenuItem(STR_C("Timetable"), "F2", &m_timetablepanel.is_open); - ImGui::MenuItem(m_debugpanel.name().c_str(), "F12", &m_debugpanel.is_open); + ImGui::MenuItem(m_scenariopanel.title.c_str(), "F3", &m_scenariopanel.is_open); + ImGui::MenuItem(m_debugpanel.name().c_str(), "Shift+F12", &m_debugpanel.is_open); ImGui::MenuItem(m_mappanel.name().c_str(), "Tab", &m_mappanel.is_open); ImGui::MenuItem(m_vehiclelist.name().c_str(), nullptr, &m_vehiclelist.is_open); ImGui::MenuItem(m_trainingcardpanel.name().c_str(), nullptr, &m_trainingcardpanel.is_open); From a68c535be9e3338306881576be4b115f5f87b0a6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 1 Nov 2026 20:12:33 +0100 Subject: [PATCH 07/32] fix(track): the cab keeps the shake a track doesn't replace, and the editor redraws all that an edit changes The side sway from the lateral acceleration now goes off only on a track with a sideways value, and the random jolts on one with a vertical or a sideways value. Dips at the rail joints and cross level alone replace neither. On a switch it goes by the path set and what its joined tracks allow. An edit in the editor also rebuilds the other switches of a joined group with the tracks they meet, and the tracks of a rail run whose joints moved. Removing a track does the same. While dragging, the tracks further off wait until the drag ends. The clatter of a bogie's axles was right: the followers stand at the pivots and the offsets are the places of the wheel sets. The misleading name goes. Also what Sonar had left: a parameter as a span, a const method, a struct and its member apart, commented out code, and a function of the grading tool split, with the same output. --- application/editormode.h | 1 + application/editormodetrack.cpp | 6 ++++-- editor/editorTrack.cpp | 22 +++++++++++++++---- editor/editorTrack.hpp | 5 ++++- tests/unit/test_track_uneven.cpp | 22 +++++++++++++++++++ tools/track_uneven_grade.py | 25 ++++++++++++++------- track_uneven_doc.md | 6 +++--- vehicle/DynObj.cpp | 18 ++++++++-------- world/Segment.h | 3 ++- world/Track.cpp | 5 ++++- world/Track.h | 11 ++++++++-- world/TrackUneven.cpp | 37 ++++++++++++++++++++++++++++++-- world/TrackUneven.h | 12 +++++++++++ 13 files changed, 140 insertions(+), 33 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index c7dbd3911..4b395ae68 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -639,6 +639,7 @@ class editor_mode : public application_mode, private editor_track::observer glm::mat4 m_track_gizmo{1.0f}; bool m_track_gizmo_using{false}; bool m_track_dirty{false}; + bool m_track_drag_on_the_way{false}; // committed, but not as the end of the drag std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; std::array, 3> m_track_materials{}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0ae732d86..2523929d8 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -442,12 +442,14 @@ void editor_mode::render_track_gizmo() void editor_mode::commit_track_drag(bool const Force) { - if (false == m_track_dirty || m_track_drag.empty()) + // on letting go the rest of what follows the dragged tracks is yet to come, even with all of the drag committed + if ((false == m_track_dirty && false == (Force && m_track_drag_on_the_way)) || m_track_drag.empty()) return; auto const now = std::chrono::steady_clock::now(); if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100)) return; - editor_track::commit(m_track_drag); + editor_track::commit(m_track_drag, false == Force); + m_track_drag_on_the_way = false == Force; m_track_dirty = false; m_track_last_commit = now; } diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index b70501747..8e6339913 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -426,7 +426,7 @@ bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason) return true; } -void editor_track::commit(std::vector const &Tracks) +void editor_track::commit(std::vector const &Tracks, bool const Dragging) { if (Tracks.empty()) return; @@ -457,6 +457,8 @@ void editor_track::commit(std::vector const &Tracks) add_unique(affected, neighbour); for (auto *track : affected) update_transition(*track); + if (false == Dragging) + add_followers(affected); for (auto *track : affected) rebuild_geometry(*track); for (auto *track : Tracks) @@ -478,6 +480,7 @@ void editor_track::commit_parameters(TTrack &Track) add_unique(affected, neighbour); for (auto *track : affected) update_transition(*track); + add_followers(affected); for (auto *track : affected) rebuild_geometry(*track); Track.mark_dirty(); @@ -2819,14 +2822,25 @@ void editor_track::retire(TTrack &Track) simulation::Region->erase_and_unregister(&Track); Track.m_editorremoved = true; for (auto *neighbour : adjoining) - { update_transition(*neighbour); - rebuild_geometry(*neighbour); - } + auto affected{adjoining}; + add_followers(affected); + for (auto *track : affected) + rebuild_geometry(*track); if (s_observer != nullptr) s_observer->track_retired(Track); } +void editor_track::add_followers(std::vector &Tracks) +{ + ++TTrack::s_layoutstamp; + for (auto *track : TTrack::switch_followers(Tracks)) + add_unique(Tracks, track); + for (auto *track : simulation::Paths.sequence()) + if (false == track->m_editorremoved && track->rail_joints_moved()) + add_unique(Tracks, track); +} + void editor_track::rebuild_geometry(TTrack &Track) { auto §ion{simulation::Region->section(Track.location())}; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 14f9e9a40..efb5fdac9 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -93,7 +93,8 @@ class editor_track static bool can_edit_geometry(TTrack const &Track, std::string &Reason); static bool is_supported(TTrack const &Track); - static void commit(std::vector const &Tracks); + // Dragging: on the way, what follows them from further off waits for the commit at the end of it + static void commit(std::vector const &Tracks, bool const Dragging = false); static void commit_parameters(TTrack &Track); static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point); @@ -350,6 +351,8 @@ class editor_track static void bind_switch_path(TTrack &Switch, int const Path); static void store_switch_path(TTrack &Switch, int const Path); static void rebuild_geometry(TTrack &Track); + // adds the tracks whose shape goes by a change of the given from further off: by the switches they meet, and by rail joints running on through them + static void add_followers(std::vector &Tracks); static TTrack *clone(TTrack const &Template); static TTrack *load_text(std::string const &Text, TTrack const *Template, std::string const &Name); static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member); diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 06f4663b0..b55de1644 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -38,6 +38,28 @@ void add_chord(std::vector &Output, std::vector const &Line, int } // namespace +TEST_CASE("uneven track: the cab keeps what the ride on the track doesn't stand in for") +{ + auto const replaced = [](double const Up, double const Side, double const Cross) { + return track_uneven::replaced_shake({Up, Side, Cross}); + }; + // all of it, as "uneven 18" + CHECK(replaced(18.0, 18.0, 18.0).sway); + CHECK(replaced(18.0, 18.0, 18.0).jolts); + // nothing at all, as on a track with dips at its joints alone + CHECK_FALSE(replaced(0.0, 0.0, 0.0).sway); + CHECK_FALSE(replaced(0.0, 0.0, 0.0).jolts); + // up and down only: there is nothing to sway with + CHECK_FALSE(replaced(18.0, 0.0, 0.0).sway); + CHECK(replaced(18.0, 0.0, 0.0).jolts); + // sideways only + CHECK(replaced(0.0, 18.0, 0.0).sway); + CHECK(replaced(0.0, 18.0, 0.0).jolts); + // cross level only turns the body + CHECK_FALSE(replaced(0.0, 0.0, 18.0).sway); + CHECK_FALSE(replaced(0.0, 0.0, 18.0).jolts); +} + TEST_CASE("uneven track: measures as the Id-14 limits for its value") { // appendix 1, class C at 80 km/h: 18 mm vertically, 17 mm sideways, 16 mm of twist on 5 m, gauge +10/-8 mm, 20 mm of cross level, J = 5.3 diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index 1994559d0..81df2ce2a 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -261,20 +261,29 @@ def plain_neighbours(t, joints): return [other for key in ("p1", "p2") for other, _, _ in joints.at(t["paths"][0][key]) if other is not t and other["kind"] == "normal"] +def joined_at(t, point, joints, seen): + # the plain tracks joined at a point, and the far ends of the straight paths of the switches joined there + plain, further = [], [] + for other, part, side in joints.at(point): + if other is t or id(part) in seen: + continue + seen.add(id(part)) + if other["kind"] != "switch": + plain.append(other) + elif other["paths"].index(part) != other["bent"]: + further.append(part["p2" if side == "p1" else "p1"]) + return plain, further + + def plain_past_switches(t, point, joints): # the plain tracks reached from a point of a track along the straight paths of switches points, seen, found = [point], set(), [] for _ in range(6): onward = [] for reached in points: - for other, part, side in joints.at(reached): - if other is t or id(part) in seen: - continue - seen.add(id(part)) - if other["kind"] != "switch": - found.append(other) - elif other["paths"].index(part) != other["bent"]: - onward.append(part["p2" if side == "p1" else "p1"]) + plain, further = joined_at(t, reached, joints, seen) + found += plain + onward += further points = onward return found diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 1c3163c50..e4822b971 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -69,12 +69,12 @@ The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the bo Models from includes (e.g. switch drives and timbers), the terrain and the catenary don't follow it, so at large values a rail shifts away from such a model by a few centimetres. -On a track with unevenness the track itself shakes the cab, in place of two of the sources of shake that were there before. It goes by the track under the vehicle, and a track with only dips at its joints counts too. On tracks with no unevenness everything works as before. +On a track with unevenness the track itself shakes the cab, in place of two of the sources of shake that were there before, as far as it has something to put in their place. It goes by the track under the vehicle, and on a switch by the path set and what its joined tracks allow. Dips at the joints and cross level replace nothing, as they don't shake the cab all the way along or sideways. On tracks with no unevenness everything works as before. | source of the cab shake | on a track with unevenness | |--------------------------------------------------------------------|----------------------------| -| side sway from the lateral acceleration: curve radius and cant, jolts sideways on a damaged switch | off | -| random jolts over 25 km/h | off | +| side sway from the lateral acceleration: curve radius and cant, jolts sideways on a damaged switch | off with a sideways value | +| random jolts over 25 km/h | off with a vertical or a sideways value | | the track itself, from the bend under the axles | added | | vertical jolts at the joints of a damaged track | stays | | fore and aft, when accelerating and braking | stays | diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 305e12020..c4838395f 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3381,7 +3381,6 @@ bool TDynamicObject::Update(double dt, double dt1) && MoverParameters->eimc[eimc_p_abed] < 0.001) FzadED = 0; //pętla bezpieczeństwa - bez ED auto const FzadPN = Fzad - FrED * MoverParameters->MED_FrED_factor; - //np = 0; std::vector PrzekrF( np ); float nPrzekrF = 0; bool test = true; @@ -8019,12 +8018,12 @@ TDynamicObject::update_shake( double const Timedelta ) { shakevector *= 0; auto const iVel { std::min( GetVelocity(), 150.0 ) }; - // an uneven track shakes the cab by itself, in place of the sway drawn from the curve and the jolts drawn at random - auto const uneven { MyTrack != nullptr && MyTrack->is_uneven() }; + // an uneven track shakes the cab by itself, in place of the sway drawn from the curve and the jolts drawn at random, as far as it does + auto const replaced { track_uneven::replaced_shake( MyTrack != nullptr ? MyTrack->uneven_underway() : track_uneven::amounts {} ) }; if( iVel > 0.5 ) { // acceleration-driven base shake shakevector += glm::dvec3( - uneven ? 0.0 : -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway + replaced.sway ? 0.0 : -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway -MoverParameters->AccVert * Timedelta * Global.ShakingMultiplierUD, -MoverParameters->AccSVBased * Timedelta * 1.5 * Global.ShakingMultiplierBF); // accent acceleration/deceleration // unevenness of the track under the outer axles @@ -8037,7 +8036,7 @@ TDynamicObject::update_shake( double const Timedelta ) { auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; - if( false == uneven && LocalRandom( iVel ) > 25.0 ) { + if( false == replaced.jolts && LocalRandom( iVel ) > 25.0 ) { // extra shake at increased velocity shake += ShakeSpring.ComputateForces( glm::dvec3( @@ -8066,11 +8065,12 @@ TDynamicObject::update_shake( double const Timedelta ) { bool TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const { - // by the outer axle on its side of the vehicle: the two can be on different tracks - auto const &outer { Axle.offset >= 0.0 ? Axle0 : Axle1 }; - if( auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; railphase >= 0.0 ) { + // from the follower on its side of the vehicle, as the two can be on different tracks. They stand fAxleDist apart, at the bogie pivots, and + // the offset of the axle is the place of its wheel set, so the two of a bogie come out half of ADist to either side of the pivot + auto const &follower { Axle.offset >= 0.0 ? Axle0 : Axle1 }; + if( auto const railphase { follower.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; railphase >= 0.0 ) { // the rails under the axle, which aren't always those under the track the vehicle is on - auto const passed { Axle.atjoints && std::abs( railphase - Axle.distance ) > 0.5 * outer.GetTrack()->fSoundDistance }; + auto const passed { Axle.atjoints && std::abs( railphase - Axle.distance ) > 0.5 * follower.GetTrack()->fSoundDistance }; Axle.distance = railphase; Axle.atjoints = true; return passed; diff --git a/world/Segment.h b/world/Segment.h index 099480e36..05522e602 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -59,7 +59,8 @@ class TSegment int path { 0 }; // which path of a switch // where the path meets the next one: the middle of their ends and their common vector across std::array>, 2> joints; - } m_setting; + }; + setting m_setting; double fLength { -1.0 }; // długość policzona std::vector fTsBuffer; // wartości parametru krzywej t dla równych odcinków s std::vector fTsSlope; // nachylenie dt/du (= dt/ds * fStep) w tych samych punktach diff --git a/world/Track.cpp b/world/Track.cpp index 49138b4eb..c9c7ae36d 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1375,7 +1375,7 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const // wypełnianie tablic VBO void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { - ++s_layoutstamp; + m_drawnrun = {}; Geometry1.clear(); Geometry2.clear(); @@ -1392,6 +1392,9 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { gfx::userdata_array empty_userdata; share_joints(); + if( rail_phase( 0.0 ) >= 0.0 ) { + m_drawnrun = m_railrun; + } // bake per-instance sleeper transforms now that the owning cell has assigned m_origin. // safe to call here even if the track has no sleepermodel (early-outs internally). if( m_sleeper_enabled && m_sleeper_local_transforms.empty() ) { diff --git a/world/Track.h b/world/Track.h index 72cbd8847..dc6be7412 100644 --- a/world/Track.h +++ b/world/Track.h @@ -348,6 +348,8 @@ public: // the unevenness the track declares, on a path of a switch track_uneven::amounts uneven_declared( int const Path = 0 ) const; bool is_uneven() const; + // the unevenness along what the vehicles take now: the path set on a switch, with what the switches joined and the tracks by them allow + track_uneven::amounts uneven_underway(); // the unevenness at a distance along a path, meeting the tracks joined at its ends track_uneven::amounts uneven_at( double const Distance, int const Path = 0 ); // how much of the displacement a point of the profile at the depth takes @@ -358,6 +360,10 @@ public: double joint_dip( double const Distance ); // m // distance from the rail joint behind; negative: the track has no joints that dip double rail_phase( double const Distance ); + // whether the rail joints are elsewhere than the geometry of the track has them + bool rail_joints_moved(); + // the uneven tracks whose values go by the switches among the given: all the switches joined to those, and the tracks they meet + static std::vector switch_followers( std::vector const &Tracks ); void ConnectionsLog(); bool DoubleSlip() const; static void fetch_default_profiles(); @@ -397,6 +403,7 @@ private: }; void settle_rail_run(); rail_run m_railrun; + rail_run m_drawnrun; // as the geometry has it; stamp -1: it has no joints that dip // the value along each path of a switch: the lowest of that path in the switches joined, 0 next to a track with none. // And at the ends of the paths, by what meets there: the other path at the common end, the path of a switch joined struct switch_stray { @@ -411,10 +418,10 @@ private: // the lowest value of the paths ending at the point track_uneven::amounts switch_paths_at( glm::dvec3 const &Point ) const; // what a plain track comes to at its end, where it meets the other one - track_uneven::amounts uneven_met( TTrack *Other, glm::dvec3 const &Point ); + track_uneven::amounts uneven_met( TTrack *Other, glm::dvec3 const &Point ) const; // the value of a switch where a track joined to it meets it track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ); - static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above + static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above. Once per change: a run is walked again after each void create_road_profile( gfx::vertex_array &Output, bool const Forcetransition = false ); void create_road_side_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const &Road, bool const Forcetransition = false ); /// diff --git a/world/TrackUneven.cpp b/world/TrackUneven.cpp index 78505ac3a..10bf48d62 100644 --- a/world/TrackUneven.cpp +++ b/world/TrackUneven.cpp @@ -10,6 +10,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/Track.h" +#include + // The part of TTrack that goes with the unevenness of a track and with what it meets at its ends: joined tracks and switches. // The rules themselves are in TrackUneven.h, here they get applied to the tracks. @@ -64,7 +66,7 @@ void keep_to_plain_track( std::array &Values, TTrack c } // the value along each path: the lowest of that path in the group, 0 in a component that a plain track joined has none of -std::array group_path_values( std::vector const &Group ) { +std::array group_path_values( std::span const Group ) { std::array values { Group.front()->uneven_declared( 0 ), Group.front()->uneven_declared( 1 ) }; for( auto const *track : Group ) { @@ -173,6 +175,11 @@ bool TTrack::is_uneven() const { return any( uneven_declared( 0 ) ) || any( uneven_declared( 1 ) ) || joint_depth() > 0.0; } +track_uneven::amounts TTrack::uneven_underway() { + + return is_switch( this ) ? switch_uneven().paths[ SwitchExtension->CurrentIndex ] : uneven_declared(); +} + double TTrack::uneven_bed_share( double const Depth ) const { return track_uneven::bed_share( Depth, std::abs( track_rail_profile( m_profile1.second ).front().position.y ), fTexHeight1 ); @@ -187,7 +194,7 @@ track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) return track_uneven::along( uneven_declared(), uneven_met( trPrev, end_point( *Segment, 0 ) ), uneven_met( trNext, end_point( *Segment, 1 ) ), Distance, Length() ); } -track_uneven::amounts TTrack::uneven_met( TTrack *Other, glm::dvec3 const &Point ) { +track_uneven::amounts TTrack::uneven_met( TTrack *Other, glm::dvec3 const &Point ) const { if( Other == nullptr ) { return {}; @@ -278,6 +285,32 @@ double TTrack::rail_phase( double const Distance ) { return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); } +bool TTrack::rail_joints_moved() { + + return m_drawnrun.stamp != -1 + && rail_phase( 0.0 ) >= 0.0 + && ( m_railrun.origin != m_drawnrun.origin || m_railrun.way != m_drawnrun.way ); +} + +std::vector TTrack::switch_followers( std::vector const &Tracks ) { + + std::vector followers; + auto const follow = [ &followers ]( TTrack *Track ) { + if( Track != nullptr && Track->is_uneven() && std::ranges::find( followers, Track ) == followers.end() ) { + followers.push_back( Track ); + } }; + for( auto *track : Tracks ) { + if( false == is_switch( track ) ) { + continue; + } + for( auto *member : switch_group( *track ) ) { + follow( member ); + std::ranges::for_each( joined_to_switch( *member ), follow ); + } + } + return followers; +} + void TTrack::settle_rail_run() { // the next track of the run past an end of a track, and which of its ends is joined there diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 2c32ad660..8e0f07750 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -43,6 +43,18 @@ inline amounts least( amounts const &Left, amounts const &Right ) { return { std::min( Left.up, Right.up ), std::min( Left.side, Right.side ), std::min( Left.cross, Right.cross ) }; } +// which of the other sources of the cab shake the ride on a track stands in for +struct shake_sources { + bool sway { false }; // from the lateral acceleration + bool jolts { false }; // drawn at random +}; + +// sideways only with something going sideways. Cross level turns the body, and the dips at the joints come and go: neither makes up for any of them +inline shake_sources replaced_shake( amounts const &Amount ) { + + return { Amount.side > 0.0, Amount.up > 0.0 || Amount.side > 0.0 }; +} + struct wave { double x; double z; From b34238f33fd49a82e9bc68380959335c890343ab Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:45:57 +0100 Subject: [PATCH 08/32] fix(app): the log written to the end of the closing, each step of it with its time; the Discord thread idle outside the driver mode The log service stopped with applicationQuitOrder, so whatever was logged while the simulator closed was lost; it now runs until StopLogService() and writes out what came before. The closing logs each of its steps with the time since it began, to tell where a closing that hangs stops. Outside the driver mode the Discord thread spun a core in its loop, it now sleeps a second between the checks. --- application/application.cpp | 16 ++++++++++++++++ nodebank_previews_standalone.cpp | 4 ++-- utilities/Logs.cpp | 11 ++++++++++- utilities/Logs.h | 2 ++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index 119cb021e..56a83c29c 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -270,7 +270,11 @@ void eu07_application::DiscordRPCService() } if (currentMode != mode::driver) + { + // the editor and the other modes: nothing to tell, without spinning a core on it + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); continue; + } // Discord RPC updater if (simulation::is_ready) @@ -690,19 +694,29 @@ void eu07_application::release_python_lock() void eu07_application::exit() { Global.applicationQuitOrder = true; + // each step of the closing in the log, to tell from it where a closing that hangs stops + auto const closing{std::chrono::steady_clock::now()}; + auto const step = [&](char const *What) { + WriteLog(std::string{"Closing: "} + What + " at " + std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - closing).count()) + " ms"); + }; + step("the modes"); for (auto &mode : m_modes) mode.reset(); + step("the renderer"); GfxRenderer->Shutdown(); m_network.reset(); // SafeDelete( simulation::Train ); + step("the scenery"); SafeDelete(simulation::Region); + step("the ui and the python workers"); ui_layer::shutdown(); // the python workers detach their GL context on the way out, so they have to be done before the windows they use are destroyed m_taskqueue.exit(); + step("the windows"); for (auto *window : m_windows) { glfwDestroyWindow(window); @@ -713,6 +727,8 @@ void eu07_application::exit() if (!Global.exec_on_exit.empty()) system(Global.exec_on_exit.c_str()); + step("done"); + StopLogService(); auto it = Global.threads.find("LogService"); if (it != Global.threads.end()) { diff --git a/nodebank_previews_standalone.cpp b/nodebank_previews_standalone.cpp index 39884fd38..d94eb13ed 100644 --- a/nodebank_previews_standalone.cpp +++ b/nodebank_previews_standalone.cpp @@ -290,9 +290,9 @@ int generate_nodebank_previews(int Argc, char *Argv[]) std::thread logservice(LogService); auto const finish = [&](int const Result) { - // give the log service time to write out what's left - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + // the log service writes out what's left before it stops Global.applicationQuitOrder = true; + StopLogService(); logservice.join(); return Result; }; diff --git a/utilities/Logs.cpp b/utilities/Logs.cpp index 38f8062b9..18020d6f1 100644 --- a/utilities/Logs.cpp +++ b/utilities/Logs.cpp @@ -73,12 +73,21 @@ std::deque ErrorStack; // lock for log stacks std::mutex logMutex; +// the log is written through the closing of the simulator, to the end +std::atomic logStop{false}; + +void StopLogService() +{ + logStop = true; +} void LogService() { - while (!Global.applicationQuitOrder) + // one more pass after the stop, so what came just before it gets written too + for (bool last{false}; false == last;) { + last = logStop; { // --- Obsługa InfoStack --- while (!InfoStack.empty()) diff --git a/utilities/Logs.h b/utilities/Logs.h index 93276d651..63727de9c 100644 --- a/utilities/Logs.h +++ b/utilities/Logs.h @@ -23,7 +23,9 @@ enum class logtype : unsigned int { traction = 1 << 9, powergrid = 1 << 10, }; +// writes the log until StopLogService(), and all of it that came before void LogService(); +void StopLogService(); void WriteLog( const char *str, logtype const Type = logtype::generic, bool isError = false ); void Error( const std::string &asMessage, bool box = false ); void Error( const char* &asMessage, bool box = false ); From e75d8496797732cb2463956f3986f022529bdb6b Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:45:57 +0100 Subject: [PATCH 09/32] fix(app): a scenery file named in another case than given is found, a scenario that can't be opened goes back to the launcher The command line gives the name of the scenery lowercased, and the sceneries made on Windows name their files and includes in any case: on a case-sensitive filesystem they weren't found. FileCased() finds the file whose name differs only in case; the parser and the scenario loader use it. A scenario that can't be opened logs the reason and goes back to the launcher instead of ending with an exception. --- application/scenarioloadermode.cpp | 16 +++++++++++++++- utilities/parser.cpp | 9 +++++++++ utilities/utilities.cpp | 14 ++++++++++++++ utilities/utilities.h | 3 +++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index dbdce6068..7afe0a442 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -41,11 +41,25 @@ bool scenarioloader_mode::update() { WriteLog("using simulation seed: " + std::to_string(Global.random_seed), logtype::generic); WriteLog("using simulation starting timestamp: " + std::to_string(Global.starting_timestamp), logtype::generic); + // the command line gives the name lowercased, the file may be named in another case, as a scenery made on Windows; + // the scenery is saved back to its own file then + if (auto const cased{FileCased(Global.asCurrentSceneryPath + Global.SceneryFile)}; cased.size() > Global.asCurrentSceneryPath.size()) + Global.SceneryFile = cased.substr(Global.asCurrentSceneryPath.size()); Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" ); WriteLog( "\nLoading scenario \"" + Global.SceneryFile + "\"..." ); timestart = std::chrono::system_clock::now(); - state = simulation::State.deserialize_begin(Global.SceneryFile); + try { + state = simulation::State.deserialize_begin(Global.SceneryFile); + } + catch (std::exception const &e) { + ErrorLog("Bad init: scenario \"" + Global.SceneryFile + "\" couldn't be opened (" + e.what() + ")"); + state.reset(); + Global.SceneryFile.clear(); + Application.pop_mode(); + Application.push_mode(eu07_application::mode::launcher); + return true; + } } try { diff --git a/utilities/parser.cpp b/utilities/parser.cpp index b64aee39b..ee5a9ce5b 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -63,6 +63,15 @@ cParser::cParser(std::string const &Stream, buffertype const Type, std::string P // the file is read in one go, tokens are then extracted straight from the memory std::ifstream file(Path, std::ios_base::binary); if (file.fail()) + { + // named in another case than it's on the disk + if (auto const cased{FileCased(Path)}; false == cased.empty()) + { + file.clear(); + file.open(cased, std::ios_base::binary); + } + } + if (file.fail()) { mFailBit = true; } diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 02608262b..0c18e0027 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -471,6 +471,20 @@ bool FileExists(std::string const &Filename) return std::filesystem::exists(Filename); } +std::string FileCased(std::string const &Filename) +{ + std::error_code error; + if (std::filesystem::exists(Filename, error)) + return Filename; + std::filesystem::path const path{Filename}; + auto const directory{path.has_parent_path() ? path.parent_path() : std::filesystem::path{"."}}; + auto const wanted{ToLower(path.filename().string())}; + for (std::filesystem::directory_iterator entry{directory, error}, end; entry != end; entry.increment(error)) + if (ToLower(entry->path().filename().string()) == wanted) + return path.has_parent_path() ? (directory / entry->path().filename()).string() : entry->path().filename().string(); + return {}; +} + std::pair FileExists(std::vector const &Names, std::vector const &Extensions) { diff --git a/utilities/utilities.h b/utilities/utilities.h index 5392ba6c5..d98b7bc9a 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -217,6 +217,9 @@ template bool extract_value(Type_ &Variable, std::string const template <> bool extract_value(bool &Variable, std::string const &Key, std::string const &Input, std::string const &Default); bool FileExists(std::string const &Filename); +// the file as it's named on the disk when only the case of its name differs, as in the sceneries made on Windows; empty when +// there's none +std::string FileCased(std::string const &Filename); std::pair FileExists(std::vector const &Names, std::vector const &Extensions); From 0d69059448b84d9b545ce0a4812c823fc7f60ec9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:45:58 +0100 Subject: [PATCH 10/32] fix(launcher): the scans of the sceneries and the vehicles go past folders they can't read, a vehicle dropped without its skin set isn't added A folder without the permission to read it or a name that can't be converted ended the scan with an exception. Ctrl+Delete with no trainset selected, a dropped skin whose vehicle is gone, and a description with a % in it no longer crash the launcher. --- launcher/scenery_list.cpp | 28 ++++++++++++++++------------ launcher/scenery_scanner.cpp | 16 ++++++++++++---- launcher/textures_scanner.cpp | 9 +++++---- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index d95c962b5..195b0e2c3 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -23,7 +23,7 @@ bool ui::scenerylist_panel::on_key(int key, int action) if (it == keyboard.keytonamemap.end()) return false; - if (Global.ctrlState && it->second == "delete") + if (Global.ctrlState && it->second == "delete" && selected_trainset != nullptr) purge_selected_trainset(); return true; @@ -117,7 +117,7 @@ void ui::scenerylist_panel::draw_launch_box() { ImGui::NextColumn(); - ImGui::TextWrapped(selected_trainset->description.c_str()); + ImGui::TextWrapped("%s", selected_trainset->description.c_str()); ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed); if (ImGui::IsItemHovered()) @@ -283,7 +283,8 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) draw_droptarget(trainset, position++); ImGui::SameLine(15.0f); - for (auto &dyn_desc : trainset.vehicles) { + for (std::size_t index = 0; index < trainset.vehicles.size(); ++index) { + auto &dyn_desc = trainset.vehicles[index]; deferred_image *mini = nullptr; if (dyn_desc.skin && dyn_desc.skin->mini.get() != deferred_image::invalid_id) @@ -345,16 +346,19 @@ void ui::scenerylist_panel::draw_droptarget(trainset_desc &trainset, int positio if (payload) { skin_set *ptr = *reinterpret_cast(payload->Data); std::shared_ptr skin; - for (auto &s : ptr->vehicle.lock()->matching_skinsets) - if (s.get() == ptr) - skin = s; + if (auto const vehicle = ptr->vehicle.lock()) + for (auto &s : vehicle->matching_skinsets) + if (s.get() == ptr) + skin = s; - trainset.vehicles.emplace(trainset.vehicles.begin() + position); - dynamic_desc &desc = trainset.vehicles[position]; + if (skin) { + trainset.vehicles.emplace(trainset.vehicles.begin() + position); + dynamic_desc &desc = trainset.vehicles[position]; - desc.name = skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); - desc.vehicle = skin->vehicle.lock(); - desc.skin = skin; + desc.name = skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); + desc.vehicle = skin->vehicle.lock(); + desc.skin = skin; + } } payload = ImGui::AcceptDragDropPayload("vehicle_set"); @@ -368,7 +372,7 @@ void ui::scenerylist_panel::draw_droptarget(trainset_desc &trainset, int positio if (moved->position < position) offset = -1; } else { - desc_copy.name = desc_copy.skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); + desc_copy.name = (desc_copy.skin ? desc_copy.skin->skin : desc_copy.name) + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); } trainset.vehicles.insert(trainset.vehicles.begin() + position + offset, desc_copy); diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index bcd18ac90..5f6a6d2f9 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -10,13 +10,21 @@ scenery_scanner::scenery_scanner(ui::vehicles_bank &bank) void scenery_scanner::scan() { - for (auto &f : std::filesystem::directory_iterator("scenery")) { - std::filesystem::path path(std::filesystem::relative(f.path(), "scenery/")); + std::error_code error; + for (std::filesystem::directory_iterator f{"scenery", std::filesystem::directory_options::skip_permission_denied, error}, end; f != end; f.increment(error)) { + std::filesystem::path path(std::filesystem::relative(f->path(), "scenery/", error)); + std::string name; + try { + name = path.string(); + } + catch (std::exception const &) { + continue; + } - if (*path.filename().string().begin() == '$') + if (name.empty() || path.filename().string().front() == '$') continue; - if (path.string().ends_with(".scn")) + if (name.ends_with(".scn")) scan_scn(path); } diff --git a/launcher/textures_scanner.cpp b/launcher/textures_scanner.cpp index 524a23ca6..741f978ca 100644 --- a/launcher/textures_scanner.cpp +++ b/launcher/textures_scanner.cpp @@ -4,12 +4,13 @@ void ui::vehicles_bank::scan_textures() { - for (auto &f : std::filesystem::recursive_directory_iterator("dynamic")) { - if (f.path().filename() != "textures.txt") + std::error_code error; + for (std::filesystem::recursive_directory_iterator f{"dynamic", std::filesystem::directory_options::skip_permission_denied, error}, end; f != end; f.increment(error)) { + if (f->path().filename() != "textures.txt") continue; - std::fstream stream(f.path(), std::ios_base::binary | std::ios_base::in); - ctx_path = std::filesystem::relative(f.path().parent_path(), "dynamic/"); + std::fstream stream(f->path(), std::ios_base::binary | std::ios_base::in); + ctx_path = std::filesystem::relative(f->path().parent_path(), "dynamic/", error); std::string line; while (std::getline(stream, line)) From e230d7b902831b9e6fdac4eebffd73087fb4b6cd Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:45:58 +0100 Subject: [PATCH 11/32] fix: a deleted model is no longer the picked node, an orthophoto tile isn't swept as a grey stub, the ED brake arrays freed Deleting a model in the editor left the renderer holding it as the picked node, the next pick used freed memory: the renderer forgets it now (Forget_Node). A texture filled straight from memory (internalsrc:) can't be read again, released by the garbage sweep it came back grey; it stays now, and the units bound with the old name are rebound. The arrays of the ED brake force sharing are vectors, they leaked once per update. The scene hierarchy and the editor menu items go through the translation. Python caches ignored. --- .gitignore | 2 ++ application/uilayer.cpp | 10 +++++----- model/Texture.cpp | 10 ++++++++++ rendering/opengl33renderer.cpp | 7 +++++++ rendering/opengl33renderer.h | 2 ++ rendering/openglrenderer.cpp | 9 +++++++++ rendering/openglrenderer.h | 2 ++ rendering/renderer.h | 2 ++ simulation/simulation.cpp | 4 ++++ vehicle/DynObj.cpp | 16 ++++------------ 10 files changed, 47 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 5186ceaee..70a6d1c68 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ vcpkg.json # Local Python (setup.bat / CMake) for embedding tools/python314/ +__pycache__/ +*.pyc diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 2dd771bdd..d8417e4fe 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -567,7 +567,7 @@ void ui_layer::render_hierarchy(){ // fitted to the contents when it first appears; no auto resize, which a docked window can't do ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_FirstUseEver); ImGui::Begin((std::string(STR_C("Scene Hierarchy")) + "###scenehierarchy").c_str(), &m_editor_hierarchy); - ImGui::Text("Registered nodes: %zu", scene::Hierarchy.size()); + ImGui::Text(STR_C("Registered nodes: %zu"), scene::Hierarchy.size()); ImGui::BeginChild("hierarchy_list", ImVec2(500, 300), true); for (auto &entry : scene::Hierarchy) @@ -598,7 +598,7 @@ void ui_layer::render_hierarchy(){ if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("UUID: %s\nNode type: %s", uuid.c_str(), typeid(*node).name()); + ImGui::SetTooltip(STR_C("UUID: %s\nNode type: %s"),uuid.c_str(), typeid(*node).name()); } } } @@ -708,14 +708,14 @@ void ui_layer::render_menu_contents() GfxRenderer->Debug_Ui_State(ret); } if(EditorModeFlag){ - ImGui::MenuItem("Hierarchy", nullptr, &m_editor_hierarchy); + ImGui::MenuItem(STR_C("Scene Hierarchy"), nullptr, &m_editor_hierarchy); bool change_history_enabled = editor_mode::change_history(); - if (ImGui::MenuItem("Change History", nullptr, &change_history_enabled)) + if (ImGui::MenuItem(STR_C("Change History"), nullptr, &change_history_enabled)) { editor_mode::set_change_history(change_history_enabled); } bool settings_open = editor_mode::settings_open(); - if (ImGui::MenuItem("Editor Settings", nullptr, &settings_open)) + if (ImGui::MenuItem(STR_C("Editor Settings"), nullptr, &settings_open)) { editor_mode::set_settings_open(settings_open); } diff --git a/model/Texture.cpp b/model/Texture.cpp index 136846a33..4a7b24792 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -706,6 +706,16 @@ void opengl_texture::update_from_memory(size_t width, size_t height, const uint8 if (glGenerateMipmap) glGenerateMipmap(target); glFlush(); + // the content given straight to the gpu can't be read again: released by the garbage sweep, the texture would come back as + // the grey stub + if (type == "internalsrc:") + { + is_static = true; + is_ready = true; + } + // the name may be a new one, and the unit active took the texture past the unit cache + for (auto &unit : units) + unit = -1; } void diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index d20454c35..a5d1f3894 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -5410,6 +5410,13 @@ void opengl33_renderer::Pick_Node_Callback(std::functionis_busy()) diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 64574420f..0f17ede7f 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -108,6 +108,8 @@ class opengl33_renderer : public gfx_renderer { Pick_Control() const override { return m_pickcontrolitem; } scene::basic_node const * Pick_Node() const override { return m_picksceneryitem; } + void + Forget_Node( scene::basic_node const *Node ) override; glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } bool diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 59152c64d..864d84141 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -1888,6 +1888,15 @@ opengl_renderer::Pick_Node_Callback( std::function m_node_pick_requests.emplace_back( Callback ); } +void +opengl_renderer::Forget_Node( scene::basic_node const *Node ) { + + if( m_picksceneryitem == Node ) { + m_picksceneryitem = nullptr; + } + std::replace_if( m_picksceneryitems.begin(), m_picksceneryitems.end(), [Node]( scene::basic_node const *Item ) { return Item == Node; }, nullptr ); +} + void opengl_renderer::Render( scene::basic_region *Region ) { diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 78fffa40b..ada032808 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -103,6 +103,8 @@ public: Pick_Control() const override { return m_pickcontrolitem; } scene::basic_node const * Pick_Node() const override { return m_picksceneryitem; } + void + Forget_Node( scene::basic_node const *Node ) override; glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } glm::mat4 diff --git a/rendering/renderer.h b/rendering/renderer.h index 0b9eb60f4..e9dc8a47c 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -90,6 +90,8 @@ public: virtual void Pick_Node_Callback( std::function Callback ) = 0; virtual auto Pick_Control() const -> TSubModel const * = 0; virtual auto Pick_Node() const -> scene::basic_node const * = 0; + // a node deleted from the scene is no longer given as picked + virtual void Forget_Node( scene::basic_node const *Node ) {} virtual auto Mouse_Position() const -> glm::dvec3 = 0; virtual auto Mouse_Hit() const -> bool { return true; } diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 70f9e3074..ea2177170 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "vehicle/Train.h" #include "application/application.h" +#include "rendering/renderer.h" #include "utilities/Logs.h" #include "vehicle/Driver.h" @@ -561,6 +562,9 @@ std::pair state_manager::rebuild_include(scene::instance_handle const } void state_manager::delete_model(TAnimModel *model) { + if (GfxRenderer) { + GfxRenderer->Forget_Node(model); + } scene::Layers.count(model->layer(), scene::layer_item::model, -1); scene::Layers.forget(model); Region->erase(model); diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index c6793e454..2309c433b 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3381,15 +3381,12 @@ bool TDynamicObject::Update(double dt, double dt1) && MoverParameters->eimc[eimc_p_abed] < 0.001) FzadED = 0; //pętla bezpieczeństwa - bez ED auto const FzadPN = Fzad - FrED * MoverParameters->MED_FrED_factor; - //np = 0; - // BUG: likely memory leak, allocation per inner loop, deleted only once outside - // TODO: sort this shit out - bool* PrzekrF = new bool[np]; + std::vector PrzekrF( np ); float nPrzekrF = 0; bool test = true; - float* FzED = new float[np]; - float* FzEP = new float[np]; - float* FmaxEP = new float[np]; + std::vector FzED( np ); + std::vector FzEP( np ); + std::vector FmaxEP( np ); // 3. ustaw pojazdom sile hamowania ED // - proporcjonalnie do mozliwosci @@ -3548,11 +3545,6 @@ bool TDynamicObject::Update(double dt, double dt1) } - delete[] PrzekrF; - delete[] FzED; - delete[] FzEP; - delete[] FmaxEP; - MED_oldFED = FzadED; } From a24118baf7630eb0ab545c947d7874135a96356d Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:50:31 +0100 Subject: [PATCH 12/32] feat(render): a geometry chunk emptied in place, the kind of its primitive told; a chunk gaining or losing its userdata gets a new buffer Erase() empties a chunk and keeps its handle to be filled again, for geometry made anew as it changes. Primitive() tells whether a chunk holds triangles or strips, -1 from a renderer which doesn't know. The VAO bank reused the buffer of a chunk of the same length even when the userdata came or went, the layout of the buffer differing then. --- rendering/geometrybank.cpp | 36 ++++++++++++++++++++++++++++++ rendering/geometrybank.h | 6 +++++ rendering/opengl33geometrybank.cpp | 3 ++- rendering/opengl33renderer.cpp | 24 +++++++++++++++++++- rendering/opengl33renderer.h | 7 ++++++ rendering/openglrenderer.cpp | 5 +++++ rendering/openglrenderer.h | 1 + rendering/renderer.h | 8 +++++++ 8 files changed, 88 insertions(+), 2 deletions(-) diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index f2db8b8bf..2a1ff5ee6 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -318,6 +318,20 @@ geometry_bank::append( gfx::vertex_array &Vertices, userdata_array &Userdata, gf return replace( Vertices, Userdata, Geometry, gfx::geometry_bank::chunk( Geometry ).vertices.size() ); } +void +geometry_bank::erase( gfx::geometry_handle const &Geometry ) { + + if( ( Geometry.chunk == 0 ) || ( Geometry.chunk > m_chunks.size() ) ) { return; } + + auto &chunk = gfx::geometry_bank::chunk( Geometry ); + if( chunk.vertices.empty() && chunk.indices.empty() ) { return; } + + gfx::vertex_array().swap( chunk.vertices ); + gfx::index_array().swap( chunk.indices ); + gfx::userdata_array().swap( chunk.userdata ); + replace_( Geometry ); +} + // draws geometry stored in specified chunk std::size_t geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) { @@ -360,6 +374,13 @@ geometry_bank::vertices( gfx::geometry_handle const &Geometry ) const { return geometry_bank::chunk( Geometry ).vertices; } +// kind of geometry of specified chunk +unsigned int +geometry_bank::primitive( gfx::geometry_handle const &Geometry ) const { + + return geometry_bank::chunk( Geometry ).type; +} + // provides direct access to vertex user data of specfied chunk userdata_array const & geometry_bank::userdata( gfx::geometry_handle const &Geometry ) const { @@ -418,6 +439,14 @@ geometrybank_manager::append( gfx::vertex_array &Vertices, userdata_array &Userd return bank( Geometry ).first->append( Vertices, Userdata, Geometry ); } + +void +geometrybank_manager::erase( gfx::geometry_handle const &Geometry ) { + + if( ( Geometry.bank == 0 ) || ( Geometry.bank > m_geometrybanks.size() ) ) { return; } + + bank( Geometry ).first->erase( Geometry ); +} // draws geometry stored in specified chunk void geometrybank_manager::draw( gfx::geometry_handle const &Geometry, unsigned int const Streams ) { @@ -483,6 +512,13 @@ geometrybank_manager::vertices( gfx::geometry_handle const &Geometry ) const { return bank( Geometry ).first->vertices( Geometry ); } +// kind of geometry of specified chunk +unsigned int +geometrybank_manager::primitive( gfx::geometry_handle const &Geometry ) const { + + return bank( Geometry ).first->primitive( Geometry ); +} + // provides direct access to vertex user data of specfied chunk gfx::userdata_array const & geometrybank_manager::userdata( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 20baf4ed2..537c79ba2 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -117,6 +117,7 @@ public: auto replace( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry, std::size_t const Offset = 0 ) -> bool; // adds supplied vertex data at the end of specified chunk auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool; + void erase( gfx::geometry_handle const &Geometry ); // draws geometry stored in specified chunk auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in specified chunk N times via glDrawElementsInstanced* @@ -138,6 +139,8 @@ public: auto vertices( gfx::geometry_handle const &Geometry ) const -> gfx::vertex_array const &; // provides direct access to vertex user data of specfied chunk auto userdata( gfx::geometry_handle const &Geometry ) const -> gfx::userdata_array const &; + // kind of geometry of specified chunk, GL_TRIANGLES, GL_TRIANGLE_STRIP... + auto primitive( gfx::geometry_handle const &Geometry ) const -> unsigned int; protected: // types: @@ -220,6 +223,7 @@ public: auto replace( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, std::size_t const Offset = 0 ) -> bool; // adds supplied vertex data at the end of specified chunk auto append( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; + void erase( gfx::geometry_handle const &Geometry ); // draws geometry stored in specified chunk void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams ); // draws geometry stored in specified chunk InstanceCount times via GPU instancing. @@ -238,6 +242,8 @@ public: auto vertices( gfx::geometry_handle const &Geometry ) const -> gfx::vertex_array const &; // provides direct access to vertex data of specfied chunk auto userdata( gfx::geometry_handle const &Geometry ) const -> gfx::userdata_array const &; + // kind of geometry of specified chunk + auto primitive( gfx::geometry_handle const &Geometry ) const -> unsigned int; // sets target texture unit for the texture data stream auto units() -> gfx::stream_units & { return m_units; } // provides access to primitives count diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 4713ec69b..978a1b75d 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -33,7 +33,8 @@ opengl33_vaogeometrybank::replace_( gfx::geometry_handle const &Geometry ) { auto &chunkrecord = m_chunkrecords[ Geometry.chunk - 1 ]; chunkrecord.is_good = false; // if the overall length of the chunk didn't change we can get away with reusing the old buffer... - if( geometry_bank::chunk( Geometry ).vertices.size() != chunkrecord.vertex_count ) { + if( ( geometry_bank::chunk( Geometry ).vertices.size() != chunkrecord.vertex_count ) + || ( geometry_bank::chunk( Geometry ).userdata.empty() == chunkrecord.has_userdata ) ) { // ...but otherwise we'll need to allocate a new one // TBD: we could keep and reuse the old buffer also if the new chunk is smaller than the old one, // but it'd require some extra tracking and work to keep all chunks up to date; also wasting vram; may be not worth it? diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index d20454c35..97b2fa8d2 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2201,8 +2201,13 @@ bool opengl33_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array return m_geometry.append(Vertices, Userdata, Geometry); } +void opengl33_renderer::Erase(gfx::geometry_handle const &Geometry) +{ + m_geometry.erase(Geometry); +} + // provides direct access to index data of specfied chunk -gfx::index_array const & opengl33_renderer::Indices(gfx::geometry_handle const &Geometry) const +gfx::index_array const & opengl33_renderer::Indices(gfx::geometry_handle const &Geometry) const { return m_geometry.indices(Geometry); } @@ -2218,6 +2223,11 @@ gfx::userdata_array const &opengl33_renderer::UserData(const gfx::geometry_handl return m_geometry.userdata(Geometry); } +int opengl33_renderer::Primitive(gfx::geometry_handle const &Geometry) const +{ + return static_cast(m_geometry.primitive(Geometry)); +} + // material methods material_handle opengl33_renderer::Fetch_Material(std::string const &Filename, bool const Loadnow) { @@ -2225,6 +2235,18 @@ material_handle opengl33_renderer::Fetch_Material(std::string const &Filename, b return m_materials.create(Filename, Loadnow); } +material_handle opengl33_renderer::Matte_Material(material_handle const Material) +{ + + return m_materials.create_matte(Material); +} + +material_handle opengl33_renderer::Tinted_Material(material_handle const Material, glm::vec3 const &Colour) +{ + + return m_materials.create_tinted(Material, Colour); +} + std::shared_ptr opengl33_renderer::Fetch_Shader(const std::string &name) { auto it = m_shaders.find(name); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 64574420f..b04153586 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -66,6 +66,7 @@ class opengl33_renderer : public gfx_renderer { bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) override; // adds supplied vertex data at the end of specified chunk bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; + void Erase( gfx::geometry_handle const &Geometry ) override; // provides direct access to index data of specfied chunk gfx::index_array const & Indices( gfx::geometry_handle const &Geometry ) const override; @@ -75,9 +76,15 @@ class opengl33_renderer : public gfx_renderer { // provides direct access to vertex data of specfied chunk gfx::userdata_array const & UserData( gfx::geometry_handle const &Geometry ) const override; + int + Primitive( gfx::geometry_handle const &Geometry ) const override; // material methods material_handle Fetch_Material( std::string const &Filename, bool const Loadnow = true ) override; + material_handle + Matte_Material( material_handle const Material ) override; + material_handle + Tinted_Material( material_handle const Material, glm::vec3 const &Colour ) override; void Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) override; IMaterial const * diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 59152c64d..51ac1e715 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -1758,6 +1758,11 @@ bool opengl_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &U return m_geometry.append( Vertices, Userdata, Geometry ); } +void opengl_renderer::Erase( gfx::geometry_handle const &Geometry ) +{ + m_geometry.erase( Geometry ); +} + // provides direct access to vertex data of specfied chunk gfx::index_array const & opengl_renderer::Indices( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 78fffa40b..da3faad06 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -65,6 +65,7 @@ public: bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) override; // adds supplied vertex data at the end of specified chunk bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; + void Erase( gfx::geometry_handle const &Geometry ) override; // provides direct access to index data of specfied chunk gfx::index_array const & Indices( gfx::geometry_handle const &Geometry ) const override; diff --git a/rendering/renderer.h b/rendering/renderer.h index 0b9eb60f4..4dabc204a 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -59,16 +59,24 @@ public: virtual auto Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) -> bool = 0; // adds supplied vertex data at the end of specified chunk virtual auto Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) -> bool = 0; + // empties specified chunk, its handle stays valid to be filled again + virtual void Erase( gfx::geometry_handle const &Geometry ) {} // provides direct access to index data of specfied chunk virtual auto Indices( gfx::geometry_handle const &Geometry ) const->gfx::index_array const & = 0; // provides direct access to vertex data of specfied chunk virtual auto Vertices( gfx::geometry_handle const &Geometry ) const ->gfx::vertex_array const & = 0; // provides direct access to vertex user data of specfied chunk virtual auto UserData( gfx::geometry_handle const &Geometry ) const ->gfx::userdata_array const & = 0; + // kind of geometry of specified chunk, -1 when the renderer doesn't tell + virtual auto Primitive( gfx::geometry_handle const &Geometry ) const -> int { return -1; } // material methods virtual auto Fetch_Material( std::string const &Filename, bool const Loadnow = true ) -> material_handle = 0; virtual void Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) = 0; virtual auto Material( material_handle const Material ) const -> IMaterial const * = 0; + // the same material with its highlights damped, for faces which aren't polished like the surfaces they are cut from. backends which can't make one give back the material itself + virtual auto Matte_Material( material_handle const Material ) -> material_handle { return Material; } + // the same material with its texture tinted by the colour, as far as the userdata of the vertices says. backends which can't make one give back the material itself + virtual auto Tinted_Material( material_handle const Material, glm::vec3 const &Colour ) -> material_handle { return Material; } // heightmap terrain: material of a chunk blending the materials of up to 8 layers by their weights. // Weights: (Samples + 1)^2 values per layer, planar, rows of growing z, the weights of a sample adding up to 255; null with a single layer. // Placement: corner of the chunk (x, z) in the texture coordinates of its mesh, and the size of the chunk. From 89c4555e1e9b410c149331cb485c5baf48f1285d Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:50:32 +0100 Subject: [PATCH 13/32] feat(render): matte and tinted copies of a material, the steel weathered or tinted as far as the userdata of its vertices says Matte_Material() gives a copy of a material with its glossiness damped and its weathering on: the faces cut out of a polished surface, as the sides of a worn rail head, darken and lose the shine by the userdata of their vertices. Tinted_Material() gives a copy whose texture is tinted to a colour as far instead. Both are made once per material. The weathering and the tint colour have a param slot of their own, param[3]: the shaders of the materials use all four components of param[2] among them (detail, parallax, water, the terrain placement), and light_common.glsl is included in all of them. MAX_PARAMS grows from 3 to 4, the model UBO by 16 bytes. Neither copy is made when another param of the shader lies on the components it would set. --- gl/ubo.h | 2 +- model/material.cpp | 117 ++++++++++++++++++++++++++++++++++++++ model/material.h | 7 +++ shaders/light_common.glsl | 18 ++++++ 4 files changed, 143 insertions(+), 1 deletion(-) diff --git a/gl/ubo.h b/gl/ubo.h index d5335b21d..9d2c96912 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -62,7 +62,7 @@ namespace gl static_assert(sizeof(scene_ubs) == 400, "bad size of ubs"); - const size_t MAX_PARAMS = 3; + const size_t MAX_PARAMS = 4; struct model_ubs { diff --git a/model/material.cpp b/model/material.cpp index dd7598ab1..ddc521e59 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -662,6 +662,123 @@ material_manager::create_from_text( std::string const &Name, std::string const & return materialhandle; } +namespace { + +opengl_material +variant_of( opengl_material const &Source, std::string const &Suffix ) { + + opengl_material variant; + variant.textures = Source.textures; + variant.texture_variants = Source.texture_variants; + variant.params = Source.params; + variant.params_state = Source.params_state; + variant.shader = Source.shader; + variant.opacity = Source.opacity; + variant.selfillum = Source.selfillum; + variant.shadow_rank = Source.shadow_rank; + variant.name = Source.name + Suffix; + variant.size = Source.size; + variant.is_good = Source.is_good; + variant.path = Source.path; + return variant; +} + +// whether another param of the shader lies on a component of the named one, which a value set for that one would change too +bool +shares_slot( std::unordered_map const &Conf, std::string const &Name ) { + + auto const own { Conf.find( Name ) }; + if( own == Conf.end() ) { + return false; + } + return std::any_of( + std::begin( Conf ), std::end( Conf ), + [&]( auto const &Entry ) { + return Entry.first != Name + && Entry.second.location == own->second.location + && Entry.second.offset < own->second.offset + own->second.size + && own->second.offset < Entry.second.offset + Entry.second.size; } ); +} + +} // namespace + +material_handle +material_manager::create_matte( material_handle const Material ) { + + if( Material == null_handle || Material >= m_materials.size() ) { + return Material; + } + auto const lookup { m_mattes.find( Material ) }; + if( lookup != m_mattes.end() ) { + return lookup->second; + } + auto matte { variant_of( m_materials[ Material ], ":matte" ) }; + matte.glossiness = 0.1f; + if( matte.shader ) { + for( auto const &[ key, value ] : std::vector>{ { "weathering", glm::vec4{ 1.f } } } ) { + auto const entry { matte.shader->param_conf.find( key ) }; + if( entry == matte.shader->param_conf.end() + || shares_slot( matte.shader->param_conf, key ) ) { + continue; + } + for( std::size_t i = 0; i < entry->second.size; ++i ) { + matte.params[ entry->second.location ][ entry->second.offset + i ] = value[ static_cast( i ) ]; + } + matte.params_state.erase( + std::remove_if( + std::begin( matte.params_state ), std::end( matte.params_state ), + [&]( gl::shader::param_entry const &State ) { + return State.location == entry->second.location && State.offset == entry->second.offset; } ), + std::end( matte.params_state ) ); + } + } + auto const mattehandle { static_cast( m_materials.size() ) }; + m_materials.emplace_back( std::move( matte ) ); + m_mattes.emplace( Material, mattehandle ); + return mattehandle; +} + +material_handle +material_manager::create_tinted( material_handle const Material, glm::vec3 const &Colour ) { + + if( Material == null_handle || Material >= m_materials.size() || !m_materials[ Material ].shader ) { + return Material; + } + auto const key { std::make_tuple( Material, Colour.r, Colour.g, Colour.b ) }; + if( auto const lookup { m_tints.find( key ) }; lookup != m_tints.end() ) { + return lookup->second; + } + // the colour in the rgb of the slot of the weathering, which tells the shader to tint by its sign; nothing else in that slot + auto const &conf { m_materials[ Material ].shader->param_conf }; + auto const weathering { conf.find( "weathering" ) }; + auto const tint { conf.find( "tint" ) }; + if( weathering == conf.end() + || tint == conf.end() + || weathering->second.offset != 3 + || tint->second.location != weathering->second.location + || tint->second.offset != 0 + || std::any_of( + std::begin( conf ), std::end( conf ), + [&]( auto const &Entry ) { + return Entry.first != weathering->first && Entry.first != tint->first && Entry.second.location == weathering->second.location; } ) ) { + return Material; + } + auto tinted { variant_of( m_materials[ Material ], ":tinted" ) }; + tinted.glossiness = m_materials[ Material ].glossiness; + auto const location { weathering->second.location }; + tinted.params[ location ] = glm::vec4{ Colour, -1.f }; + tinted.params_state.erase( + std::remove_if( + std::begin( tinted.params_state ), std::end( tinted.params_state ), + [&]( gl::shader::param_entry const &State ) { + return State.location == location; } ), + std::end( tinted.params_state ) ); + auto const tintedhandle { static_cast( m_materials.size() ) }; + m_materials.emplace_back( std::move( tinted ) ); + m_tints.emplace( key, tintedhandle ); + return tintedhandle; +} + void material_manager::on_weather_change() { diff --git a/model/material.h b/model/material.h index f329a3256..05a68880f 100644 --- a/model/material.h +++ b/model/material.h @@ -142,6 +142,11 @@ public: // material described by provided text (the content of a .mat file), not kept in a file. returns: handle to the material or null_handle material_handle create_from_text( std::string const &Name, std::string const &Definition ); + // copy of provided material with its specular highlights and reflections damped. returns: handle to the copy, made once per material + material_handle + create_matte( material_handle const Material ); + material_handle + create_tinted( material_handle const Material, glm::vec3 const &Colour ); opengl_material const & material( material_handle const Material ) const { return m_materials[ Material ]; } opengl_material & @@ -170,6 +175,8 @@ private: // members: material_sequence m_materials; index_map m_materialmappings; + std::unordered_map m_mattes; + std::map, material_handle> m_tints; }; //--------------------------------------------------------------------------- diff --git a/shaders/light_common.glsl b/shaders/light_common.glsl index 0801b09dc..6261a090d 100644 --- a/shaders/light_common.glsl +++ b/shaders/light_common.glsl @@ -9,6 +9,11 @@ uniform sampler2D headlightmap; float glossiness = 1.0; float metalic = 0.0; +in vec4 UserData; +// a slot of its own, the shaders of the materials using all of param[2]: how weathered the steel is as far as the userdata of +// its vertices says; below 0: the texture tinted to the colour of tint as far instead +#param (weathering, 3, 3, 1, zero) +#param (tint, 3, 0, 3, one) // --------------------------------------------------------------------- // Lighting balance tunables - tweak these to control overall scene @@ -285,6 +290,19 @@ vec3 apply_lights(vec3 fragcolor, vec3 fragnormal, vec3 texturecolor, float refl { // Diffuse colour is applied once, to the albedo; the light terms below are not scaled by it again texturecolor *= material_diffuse(); + float weathered = (param[3].w > 0.0 ? clamp(param[3].w * UserData.x, 0.0, 1.0) : 0.0); + float tinted = (param[3].w < 0.0 ? clamp(-param[3].w * UserData.x, 0.0, 1.0) : 0.0); + // a tint only scales the colour of the texture, the steel keeps its own polish + float polished = weathered * smoothstep(0.03, 0.12, dot(texturecolor, vec3(0.2126, 0.7152, 0.0722))); + float weatheredluma = min(dot(texturecolor, vec3(0.2126, 0.7152, 0.0722)), 0.05); + vec3 unused = texturecolor * (weatheredluma / max(dot(texturecolor, vec3(0.2126, 0.7152, 0.0722)), 1e-4)); + unused = mix(unused, vec3(1.0, 0.82, 0.74) * (weatheredluma / 0.8525), 0.5); + texturecolor = mix(texturecolor, unused, smoothstep(0.0, 0.4, weathered)); + texturecolor = (tinted > 0.0 ? texturecolor * mix(vec3(1.0), param[3].rgb, tinted) : texturecolor); + specularity = mix(specularity, min(specularity, 0.03), smoothstep(0.0, 0.4, polished)); + metalic = mix(metalic, 0.0, polished); + glossiness = mix(glossiness, min(glossiness, 0.06 * abs(param[1].w)), polished); + reflectivity = mix(reflectivity, min(reflectivity, 0.08 * param[1].z), smoothstep(0.0, 0.4, polished)); // Scale ambient before it gets tinted by the texture. // Sun, headlights and emission are added afterwards so they are NOT // attenuated by AMBIENT_SCALE - this only dims the indirect term. From ed0ff36a6994243d939160807c651376adecc7d2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 21:53:52 +0100 Subject: [PATCH 14/32] fix(app): a scenario failing as it loads goes back to the launcher too, the files of other cases looked for only off Windows A scenario failing past its opening popped the loader mode and went on to pop it again and push the driver mode with the scenery half loaded; it goes back to the launcher as one that can't be opened. FileCased() goes through the folder only where the file system tells the case apart: on Windows a missing file was looked for in its whole folder for nothing, and a name outside the code page threw from the parser. The Discord thread sleeps a quarter of a second outside the driver mode, not to hold the closing up. --- application/application.cpp | 2 +- application/scenarioloadermode.cpp | 5 +++++ application/uilayer.cpp | 2 +- utilities/utilities.cpp | 7 +++++++ utilities/utilities.h | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index 56a83c29c..33f5c8a0e 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -272,7 +272,7 @@ void eu07_application::DiscordRPCService() if (currentMode != mode::driver) { // the editor and the other modes: nothing to tell, without spinning a core on it - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + std::this_thread::sleep_for(std::chrono::milliseconds(250)); continue; } diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index 7afe0a442..eaa034f7e 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -67,8 +67,13 @@ bool scenarioloader_mode::update() { return true; } catch (invalid_scenery_exception &e) { + // 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(); + Global.SceneryFile.clear(); Application.pop_mode(); + Application.push_mode(eu07_application::mode::launcher); + return true; } // the loading state holds the scenario file open, which on some systems keeps the file from being replaced // when the scenery is saved in the editor. it'd also get in the way of another load diff --git a/application/uilayer.cpp b/application/uilayer.cpp index d8417e4fe..a188d4f0b 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -598,7 +598,7 @@ void ui_layer::render_hierarchy(){ if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(STR_C("UUID: %s\nNode type: %s"),uuid.c_str(), typeid(*node).name()); + ImGui::SetTooltip(STR_C("UUID: %s\nNode type: %s"), uuid.c_str(), typeid(*node).name()); } } } diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 0c18e0027..b25c6b31d 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -476,6 +476,12 @@ std::string FileCased(std::string const &Filename) std::error_code error; if (std::filesystem::exists(Filename, error)) return Filename; +#ifdef _WIN32 + // the names there are found in any case already: no folder to go through for every file missing, nor names outside of the + // code page to throw on + return {}; +#else + // only the name of the file itself, a folder named in another case isn't found std::filesystem::path const path{Filename}; auto const directory{path.has_parent_path() ? path.parent_path() : std::filesystem::path{"."}}; auto const wanted{ToLower(path.filename().string())}; @@ -483,6 +489,7 @@ std::string FileCased(std::string const &Filename) if (ToLower(entry->path().filename().string()) == wanted) return path.has_parent_path() ? (directory / entry->path().filename()).string() : entry->path().filename().string(); return {}; +#endif } std::pair FileExists(std::vector const &Names, std::vector const &Extensions) diff --git a/utilities/utilities.h b/utilities/utilities.h index d98b7bc9a..f16431dde 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -218,7 +218,7 @@ template <> bool extract_value(bool &Variable, std::string const &Key, std::stri bool FileExists(std::string const &Filename); // the file as it's named on the disk when only the case of its name differs, as in the sceneries made on Windows; empty when -// there's none +// there's none. the folders have to be named in their case; on Windows, finding the names in any case itself, the file given or none std::string FileCased(std::string const &Filename); std::pair FileExists(std::vector const &Names, std::vector const &Extensions); From 48af69cbf19e475d4472d6347e64f29b770a1ddf Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 22:00:48 +0100 Subject: [PATCH 15/32] fix(render): the matte and tinted copies of a material made again with its season and weather, the primitive of a handle to no chunk told as none The copies don't update themselves, their names aren't files to read again: after a change of the weather or the season the material got its new textures and its copies kept the old ones. They are made again from it then, under their handles. primitive() of an empty handle or one past the banks read out of the bank list; it gives no_primitive there, -1 from the renderer. --- model/material.cpp | 131 ++++++++++++++++++++++----------- model/material.h | 5 ++ rendering/geometrybank.cpp | 4 + rendering/geometrybank.h | 3 +- rendering/opengl33renderer.cpp | 3 +- 5 files changed, 102 insertions(+), 44 deletions(-) diff --git a/model/material.cpp b/model/material.cpp index ddc521e59..2b92986d0 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -676,6 +676,7 @@ variant_of( opengl_material const &Source, std::string const &Suffix ) { variant.opacity = Source.opacity; variant.selfillum = Source.selfillum; variant.shadow_rank = Source.shadow_rank; + variant.glossiness = Source.glossiness; variant.name = Source.name + Suffix; variant.size = Source.size; variant.is_good = Source.is_good; @@ -700,6 +701,65 @@ shares_slot( std::unordered_map const &Con && own->second.offset < Entry.second.offset + Entry.second.size; } ); } +// the highlights of a copy of a material damped and its weathering on +void +make_matte( opengl_material &Variant ) { + + Variant.glossiness = 0.1f; + if( !Variant.shader ) { + return; + } + for( auto const &[ key, value ] : std::vector>{ { "weathering", glm::vec4{ 1.f } } } ) { + auto const entry { Variant.shader->param_conf.find( key ) }; + if( entry == Variant.shader->param_conf.end() + || shares_slot( Variant.shader->param_conf, key ) ) { + continue; + } + for( std::size_t i = 0; i < entry->second.size; ++i ) { + Variant.params[ entry->second.location ][ entry->second.offset + i ] = value[ static_cast( i ) ]; + } + Variant.params_state.erase( + std::remove_if( + std::begin( Variant.params_state ), std::end( Variant.params_state ), + [&]( gl::shader::param_entry const &State ) { + return State.location == entry->second.location && State.offset == entry->second.offset; } ), + std::end( Variant.params_state ) ); + } +} + +// the colour in the rgb of the slot of the weathering of a copy of a material, which tells the shader to tint by its sign. returns: +// whether the shader can tint it, the weathering and the tint alone in that slot +bool +make_tinted( opengl_material &Variant, glm::vec3 const &Colour ) { + + if( !Variant.shader ) { + return false; + } + auto const &conf { Variant.shader->param_conf }; + auto const weathering { conf.find( "weathering" ) }; + auto const tint { conf.find( "tint" ) }; + if( weathering == conf.end() + || tint == conf.end() + || weathering->second.offset != 3 + || tint->second.location != weathering->second.location + || tint->second.offset != 0 + || std::any_of( + std::begin( conf ), std::end( conf ), + [&]( auto const &Entry ) { + return Entry.first != weathering->first && Entry.first != tint->first && Entry.second.location == weathering->second.location; } ) ) { + return false; + } + auto const location { weathering->second.location }; + Variant.params[ location ] = glm::vec4{ Colour, -1.f }; + Variant.params_state.erase( + std::remove_if( + std::begin( Variant.params_state ), std::end( Variant.params_state ), + [&]( gl::shader::param_entry const &State ) { + return State.location == location; } ), + std::end( Variant.params_state ) ); + return true; +} + } // namespace material_handle @@ -713,25 +773,7 @@ material_manager::create_matte( material_handle const Material ) { return lookup->second; } auto matte { variant_of( m_materials[ Material ], ":matte" ) }; - matte.glossiness = 0.1f; - if( matte.shader ) { - for( auto const &[ key, value ] : std::vector>{ { "weathering", glm::vec4{ 1.f } } } ) { - auto const entry { matte.shader->param_conf.find( key ) }; - if( entry == matte.shader->param_conf.end() - || shares_slot( matte.shader->param_conf, key ) ) { - continue; - } - for( std::size_t i = 0; i < entry->second.size; ++i ) { - matte.params[ entry->second.location ][ entry->second.offset + i ] = value[ static_cast( i ) ]; - } - matte.params_state.erase( - std::remove_if( - std::begin( matte.params_state ), std::end( matte.params_state ), - [&]( gl::shader::param_entry const &State ) { - return State.location == entry->second.location && State.offset == entry->second.offset; } ), - std::end( matte.params_state ) ); - } - } + make_matte( matte ); auto const mattehandle { static_cast( m_materials.size() ) }; m_materials.emplace_back( std::move( matte ) ); m_mattes.emplace( Material, mattehandle ); @@ -748,43 +790,47 @@ material_manager::create_tinted( material_handle const Material, glm::vec3 const if( auto const lookup { m_tints.find( key ) }; lookup != m_tints.end() ) { return lookup->second; } - // the colour in the rgb of the slot of the weathering, which tells the shader to tint by its sign; nothing else in that slot - auto const &conf { m_materials[ Material ].shader->param_conf }; - auto const weathering { conf.find( "weathering" ) }; - auto const tint { conf.find( "tint" ) }; - if( weathering == conf.end() - || tint == conf.end() - || weathering->second.offset != 3 - || tint->second.location != weathering->second.location - || tint->second.offset != 0 - || std::any_of( - std::begin( conf ), std::end( conf ), - [&]( auto const &Entry ) { - return Entry.first != weathering->first && Entry.first != tint->first && Entry.second.location == weathering->second.location; } ) ) { + auto tinted { variant_of( m_materials[ Material ], ":tinted" ) }; + if( false == make_tinted( tinted, Colour ) ) { return Material; } - auto tinted { variant_of( m_materials[ Material ], ":tinted" ) }; - tinted.glossiness = m_materials[ Material ].glossiness; - auto const location { weathering->second.location }; - tinted.params[ location ] = glm::vec4{ Colour, -1.f }; - tinted.params_state.erase( - std::remove_if( - std::begin( tinted.params_state ), std::end( tinted.params_state ), - [&]( gl::shader::param_entry const &State ) { - return State.location == location; } ), - std::end( tinted.params_state ) ); auto const tintedhandle { static_cast( m_materials.size() ) }; m_materials.emplace_back( std::move( tinted ) ); m_tints.emplace( key, tintedhandle ); return tintedhandle; } +void +material_manager::update_variants( bool const Weather ) { + + // the copies don't update themselves, their names aren't files to read again + auto const changed = [&]( material_handle const Source ) { + auto const &source { m_materials[ Source ] }; + return Weather ? source.update_on_weather_change : source.update_on_season_change; }; + for( auto const &[ source, matte ] : m_mattes ) { + if( changed( source ) ) { + auto variant { variant_of( m_materials[ source ], ":matte" ) }; + make_matte( variant ); + m_materials[ matte ] = std::move( variant ); + } + } + for( auto const &[ key, tinted ] : m_tints ) { + auto const source { std::get<0>( key ) }; + if( changed( source ) ) { + auto variant { variant_of( m_materials[ source ], ":tinted" ) }; + make_tinted( variant, glm::vec3 { std::get<1>( key ), std::get<2>( key ), std::get<3>( key ) } ); + m_materials[ tinted ] = std::move( variant ); + } + } +} + void material_manager::on_weather_change() { for( auto &material : m_materials ) { if( material.update_on_weather_change ) { material.update(); } } + update_variants( true ); } void @@ -793,6 +839,7 @@ material_manager::on_season_change() { for( auto &material : m_materials ) { if( material.update_on_season_change ) { material.update(); } } + update_variants( false ); } // checks whether specified material is in the material bank. returns handle to the material, or a null handle diff --git a/model/material.h b/model/material.h index 05a68880f..82d14557c 100644 --- a/model/material.h +++ b/model/material.h @@ -145,6 +145,8 @@ public: // copy of provided material with its specular highlights and reflections damped. returns: handle to the copy, made once per material material_handle create_matte( material_handle const Material ); + // copy of provided material with its texture tinted to the colour as far as the userdata of the vertices says. returns: handle to + // the copy, made once per material and colour; the material itself when its shader can't tint it material_handle create_tinted( material_handle const Material, glm::vec3 const &Colour ); opengl_material const & @@ -166,6 +168,9 @@ private: // checks whether specified texture is in the texture bank. returns texture id, or npos. material_handle find_in_databank( std::string const &Materialname ) const; + // the matte and tinted copies of the materials changed with the weather or the season made again from them, under their handles + void + update_variants( bool const Weather ); public: // checks whether specified file exists. returns name of the located file, or empty string. static std::pair diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index 2a1ff5ee6..aa7c1f8b5 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -378,6 +378,8 @@ geometry_bank::vertices( gfx::geometry_handle const &Geometry ) const { unsigned int geometry_bank::primitive( gfx::geometry_handle const &Geometry ) const { + if( ( Geometry.chunk == 0 ) || ( Geometry.chunk > m_chunks.size() ) ) { return no_primitive; } + return geometry_bank::chunk( Geometry ).type; } @@ -516,6 +518,8 @@ geometrybank_manager::vertices( gfx::geometry_handle const &Geometry ) const { unsigned int geometrybank_manager::primitive( gfx::geometry_handle const &Geometry ) const { + if( ( Geometry.bank == 0 ) || ( Geometry.bank > m_geometrybanks.size() ) ) { return geometry_bank::no_primitive; } + return bank( Geometry ).first->primitive( Geometry ); } diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 537c79ba2..c01e46fe3 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -139,8 +139,9 @@ public: auto vertices( gfx::geometry_handle const &Geometry ) const -> gfx::vertex_array const &; // provides direct access to vertex user data of specfied chunk auto userdata( gfx::geometry_handle const &Geometry ) const -> gfx::userdata_array const &; - // kind of geometry of specified chunk, GL_TRIANGLES, GL_TRIANGLE_STRIP... + // kind of geometry of specified chunk, GL_TRIANGLES, GL_TRIANGLE_STRIP...; no_primitive for a handle to no chunk auto primitive( gfx::geometry_handle const &Geometry ) const -> unsigned int; + static constexpr unsigned int no_primitive { ~0u }; protected: // types: diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 97b2fa8d2..9b42bb280 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2225,7 +2225,8 @@ gfx::userdata_array const &opengl33_renderer::UserData(const gfx::geometry_handl int opengl33_renderer::Primitive(gfx::geometry_handle const &Geometry) const { - return static_cast(m_geometry.primitive(Geometry)); + auto const primitive{m_geometry.primitive(Geometry)}; + return primitive == gfx::geometry_bank::no_primitive ? -1 : static_cast(primitive); } // material methods From 85233160069bd0d5b79dcb34800363431eebb61e Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 3 Nov 2026 23:03:52 +0100 Subject: [PATCH 16/32] fix(track): a tributary loads as a river of its first path keeping the second, a track of an unknown type is left out, where both made the simulator fall over as it loaded On master neither got a segment: update_location() dereferenced the missing one of a track of an unknown type, and a tributary went to Switch(0), which logged "Cannot switch normal track", with no segment under it either. A tributary is now a river of its first path. Its second path is kept in m_paths, isn't drawn nor run on, and is written back with the type "tributary". A track of an unknown type is logged and left out by the loader; as it is no node, its text stays in the file when the editor saves the scenery. --- simulation/simulationstateserializer.cpp | 6 ++++++ world/Track.cpp | 18 ++++++++++++++++-- world/Track.h | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 1ff771d1d..78955c828 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -543,6 +543,12 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch else if( nodedata.type == "track" ) { auto *path { deserialize_path( Input, Scratchpad, nodedata ) }; + if( path->eType == tt_Unknown ) { + // with no path made it can't be joined nor drawn, and nothing could run on it + ErrorLog( "Bad scenario: track \"" + path->name() + "\" of an unknown type defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ") left out" ); + delete path; + return; + } // duplicates of named tracks are currently experimentally allowed if( false == simulation::Paths.insert( path ) ) { ErrorLog( "Bad scenario: duplicate track name \"" + path->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); diff --git a/world/Track.cpp b/world/Track.cpp index f1a29798c..14998ce7c 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -427,11 +427,15 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } else if (str == "tributary") { - eType = tt_Tributary; + // dopływ: no path of it was ever made, the simulator falling over it as it loaded. a river of its first path now, the + // second one kept and written back + ErrorLog("Bad track: tributary \"" + m_name + "\" in file \"" + parser->Name() + "\" (line " + std::to_string(parser->Line()) + ") is drawn and run on as a river of its first path"); + eType = tt_Normal; iCategoryFlag = 4; + m_tributary = true; } else - eType = tt_Unknown; + eType = tt_Unknown; // left out by the loader if (Global.iWriteLogEnabled & 4) WriteLog(str); parser->getTokens(4); @@ -517,6 +521,11 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) break; } } + if( m_tributary ) { + // the second path of a tributary, kept to be written back + m_paths.emplace_back(); + m_paths.back().deserialize( *parser, pOrigin ); + } init_segments( true ); @@ -681,6 +690,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_name = m_name.substr(0, i - 1); // usunięcie z nazwy } + if( CurrentSegment() == nullptr ) { + // of an unknown type, no path of it is made + return; + } update_location(); // sleeper transforms are baked later in create_geometry(), once the owning cell has // assigned this track its m_origin (otherwise the local-space matrices would be relative @@ -2314,6 +2327,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { // type Output << ( eType == tt_Normal ? ( + m_tributary ? "tributary" : iCategoryFlag == 1 ? "normal" : iCategoryFlag == 2 ? "road" : iCategoryFlag == 4 ? "river" : diff --git a/world/Track.h b/world/Track.h index 72a6b7ff4..5952da6a7 100644 --- a/world/Track.h +++ b/world/Track.h @@ -187,6 +187,7 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; + bool m_tributary { false }; // loaded as a tributary: a river of its first path, the second one kept to be written back scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; From 09b1c2de10463f69331841a81cc18999fd7846a2 Mon Sep 17 00:00:00 2001 From: maj00r <73280030+maj00r@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:12:05 +0100 Subject: [PATCH 17/32] fix(utilities): win1250_to_utf8 turns the polish letters into utf-8, not into themselves (#252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utilities.cpp starts with a byte order mark, so the compiler reads it as utf-8. The game's targets don't get /utf-8, though: set_target_properties(... COMPILE_OPTIONS ...) in the MSVC block of CMakeLists.txt replaces the options add_compile_options gave them. So the string literals went into the program in the code page of the system, windows-1250 on a polish one, and the map of win1250_to_utf8 turned each letter into itself. What read windows-1250 through it got no utf-8: the names, descriptions and categories of the sceneries and the names of the textures in the launcher, and the descriptions of the templates in the editor. The editor writes such a description back through utf8_to_win1250, which makes a question mark of each byte that doesn't start a utf-8 sequence, so the letters it had read unchanged didn't come back. The utf-8 sequences are written as bytes now, the way utf8_to_win1250 has them, so they don't depend on the options. The options stay as they are: with /utf-8 other literals of the files with a byte order mark would change, the "±" of the table of Bezogonkow would take two bytes and move the letters after it. The unit tests check the letters and that the text given is valid utf-8. Co-authored-by: maj00r --- tests/unit/test_win1250.cpp | 26 ++++++++++++++++++++++++++ utilities/utilities.cpp | 7 +++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_win1250.cpp diff --git a/tests/unit/test_win1250.cpp b/tests/unit/test_win1250.cpp new file mode 100644 index 000000000..156c46ae7 --- /dev/null +++ b/tests/unit/test_win1250.cpp @@ -0,0 +1,26 @@ +#include "stdafx.h" +#include "doctest.h" +#include "utilities/utilities.h" + +// the polish letters, in windows-1250 and in utf-8; written as bytes, the build reads the sources in any code page +namespace +{ +std::string const kWin1250{"\xA5\xC6\xCA\xA3\xD1\xD3\x8C\x8F\xAF\xB9\xE6\xEA\xB3\xF1\xF3\x9C\x9F\xBF"}; +std::string const kUtf8{"\xC4\x84\xC4\x86\xC4\x98\xC5\x81\xC5\x83\xC3\x93\xC5\x9A\xC5\xB9\xC5\xBB" + "\xC4\x85\xC4\x87\xC4\x99\xC5\x82\xC5\x84\xC3\xB3\xC5\x9B\xC5\xBA\xC5\xBC"}; +} // namespace + +TEST_CASE("win1250: the polish letters of a scenery file read as utf-8") { + CHECK(win1250_to_utf8("Bia\xB3" "a Podlaska") == "Bia\xC5\x82" "a Podlaska"); + CHECK(win1250_to_utf8(kWin1250) == kUtf8); + // the rest of the text stays as it is + CHECK(win1250_to_utf8("tor 1, km 0.178") == "tor 1, km 0.178"); +} + +TEST_CASE("win1250: what win1250_to_utf8 gives is valid utf-8") { + std::u32string decoded; + CHECK_FALSE(utf8_to_utf32(kWin1250, decoded)); + REQUIRE(utf8_to_utf32(win1250_to_utf8(kWin1250), decoded)); + CHECK(decoded.size() == kWin1250.size()); + CHECK(decoded[12] == U'\x0142'); +} diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 02608262b..e84307e39 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -317,8 +317,11 @@ void win1250_to_ascii(std::string &Input) std::string win1250_to_utf8(const std::string &Input) { - std::unordered_map const charmap{{165, "Ą"}, {198, "Ć"}, {202, "Ę"}, {163, "Ł"}, {209, "Ń"}, {211, "Ó"}, {140, "Ś"}, {143, "Ź"}, {175, "Ż"}, - {185, "ą"}, {230, "ć"}, {234, "ę"}, {179, "ł"}, {241, "ń"}, {243, "ó"}, {156, "ś"}, {159, "ź"}, {191, "ż"}}; + // the utf-8 sequences written as bytes: the file has a byte order mark and the build doesn't give the compiler /utf-8, so letters + // written as letters would end up in the program in windows-1250, the same as the input + static std::unordered_map const charmap{{'\xA5', "\xC4\x84"}, {'\xC6', "\xC4\x86"}, {'\xCA', "\xC4\x98"}, {'\xA3', "\xC5\x81"}, {'\xD1', "\xC5\x83"}, {'\xD3', "\xC3\x93"}, + {'\x8C', "\xC5\x9A"}, {'\x8F', "\xC5\xB9"}, {'\xAF', "\xC5\xBB"}, {'\xB9', "\xC4\x85"}, {'\xE6', "\xC4\x87"}, {'\xEA', "\xC4\x99"}, + {'\xB3', "\xC5\x82"}, {'\xF1', "\xC5\x84"}, {'\xF3', "\xC3\xB3"}, {'\x9C', "\xC5\x9B"}, {'\x9F', "\xC5\xBA"}, {'\xBF', "\xC5\xBC"}}; std::string output; std::unordered_map::const_iterator lookup; for (auto &input : Input) From fc22645d7c5a1fa7c008436b48541296bbbec040 Mon Sep 17 00:00:00 2001 From: pawelw Date: Thu, 5 Nov 2026 10:25:26 +0100 Subject: [PATCH 18/32] fix(physics): articulated couplers stall freight trains (eu07/maszyna#553) (#255) * fix(physics): articulated couplers stall freight trains (eu07/maszyna#553) Articulated wagons (T3000e, N-230, N-234) set up with CType=Articulated barely moved in a freight train and jerked. - The articulated joint took the spring force up to DmaxC = 0.05 m only, 225 kN, well short of its FmaxC of 850 kN and of what a loaded section in a freight train takes. It overstretched and the sections collided at every physics step. Dmax is now sized so the spring carries FmaxC (0.19 m stretched, 0.09 m pressed in), never below the old 0.05 m. - ComputeCollision() scaled the velocities of both vehicles by sqrt(1 - beta), so each collision braked the pair as a whole: two coupled vehicles in a sustained "collision" lost a third of their speed per step. The restitution now applies to the velocities relative to the centre of mass only, and the pair keeps its momentum. Tests: the articulated joint carries FmaxC within its travel; a jerked articulated set and a jerked screw-coupled pair keep their momentum. On the code before the fix both pairs stop dead within 3 s. * style(physics): English-only comment in ComputeCollision() The comment above the collision velocities started in Polish and went on in English. * style(physics): SonarQube findings in the #553 fix std::lerp for the collision velocities (cpp:S6179) and test locals renamed so they no longer shadow the end and coupling enumerators (cpp:S1117). --- McZapkie/Mover.cpp | 17 ++++++--- tests/unit/test_couplers.cpp | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index cb17e30ad..954e277ba 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/parser.h" #include "simulation/simulation.h" +#include #include //--------------------------------------------------------------------------- @@ -51,8 +52,13 @@ double ComputeCollision(double &v1, double &v2, double m1, double m2, double bet double sum = m1 + m2; double w1 = (m2 * v2 * 2.0 + v1 * (m1 - m2)) / sum; double w2 = (m1 * v1 * 2.0 + v2 * (m2 - m1)) / sum; - v1 = w1 * std::sqrt(1.0 - beta); // niejawna zmiana predkosci wskutek zderzenia - v2 = w2 * std::sqrt(1.0 - beta); + // the collision changes the velocities directly: the bounce loses energy in the velocities relative to the + // centre of mass only, the pair keeps its momentum. scaling the velocities themselves braked the vehicles + // down with every step their couplers spent stretched or pressed in all the way (eu07/maszyna#553) + auto const centre{(m1 * v1 + m2 * v2) / sum}; + auto const restitution{std::sqrt(1.0 - beta)}; + v1 = std::lerp(centre, w1, restitution); + v2 = std::lerp(centre, w2, restitution); return m1 * (w2 - w1) * (1 - beta); } } @@ -10695,12 +10701,15 @@ void TMoverParameters::LoadFIZ_BuffCoupl(std::string const &line, int const Inde coupler->beta = 0.55; */ thiscoupler->SpringKC = 4500 * 1000; - thiscoupler->DmaxC = 0.05; thiscoupler->FmaxC = 850 * 1000; thiscoupler->SpringKB = 9200 * 1000; - thiscoupler->DmaxB = 0.05; thiscoupler->FmaxB = 320 * 1000; thiscoupler->beta = 0.55; + // the spring force is taken up to Dmax only, past it the vehicles collide (CollisionDetect()); so the joint + // carries the full FmaxC either way before it gives. with 0.05 m it carried 225 kN, short of what a loaded + // articulated wagon in a freight train takes, which then collided at every step (eu07/maszyna#553) + thiscoupler->DmaxC = std::max(0.05, thiscoupler->FmaxC / thiscoupler->SpringKC); + thiscoupler->DmaxB = std::max(0.05, thiscoupler->FmaxC / thiscoupler->SpringKB); } if (Index == 0) diff --git a/tests/unit/test_couplers.cpp b/tests/unit/test_couplers.cpp index 8787a419b..2a0ff6e50 100644 --- a/tests/unit/test_couplers.cpp +++ b/tests/unit/test_couplers.cpp @@ -456,3 +456,72 @@ TEST_CASE("couplers: the coupler force of vehicles facing each other lands on th CHECK(second->Couplers[end::front].CForce == 0.0); } } + +// eu07/maszyna#553: articulated wagons (T3000e, N-234 and the like) set up with articulated couplers barely moved in a +// freight train. the joint took the spring force up to 5 cm only, 225 kN, so it overstretched under the train and the +// sections collided at every step; and each collision scaled the velocities of both vehicles down, rather than only +// the velocity of one relative to the other, braking the whole train + +namespace { + +// the wagon from data/wagon.json, loaded, with articulated couplers at both ends +vehicle articulated_wagon(double Velocity, std::string const &Name) { + return vehicle{definition::load("wagon").set("Param", "M", 60000).set("BuffCoupl", "CType", "Articulated"), Velocity, Name}; +} + +// couples the facing ends of the two vehicles for good, as the scenery does for an articulated set +void join(vehicle &First, int FirstEnd, vehicle &Second, int SecondEnd) { + REQUIRE(First->Attach(FirstEnd, SecondEnd, &Second.mover(), coupling::coupler, true, false)); + REQUIRE(Second->Attach(SecondEnd, FirstEnd, &First.mover(), coupling::coupler, true, false)); +} + +} // namespace + +TEST_CASE("couplers: an articulated joint takes its full tensile force within its travel") { + auto wagon{articulated_wagon(0.0, "wagon")}; + auto const &joint{wagon->Couplers[end::front]}; + REQUIRE(joint.type() == TCouplerType::Articulated); + + CHECK(joint.SpringKC * joint.DmaxC >= doctest::Approx(joint.FmaxC)); + CHECK(joint.SpringKB * joint.DmaxB >= doctest::Approx(joint.FmaxC)); + // and no less than it had before + CHECK(joint.DmaxC >= 0.05); + CHECK(joint.DmaxB >= 0.05); +} + +TEST_CASE("couplers: an articulated set keeps its momentum when one section jerks the other") { + // the front section moving, the rear one barely (so that its brakes are released), joined: the jerk overstretches the joint, the sections collide + // and run on together at half the speed, as the momentum they have between them dictates + auto front_section{articulated_wagon(20.0, "front")}; + auto rear_section{articulated_wagon(0.1, "rear")}; + join(rear_section, end::front, front_section, end::rear); + test::pair pair{rear_section, end::front, front_section, end::rear, 0.0}; + REQUIRE(pair.coupled()); + // brakes released, as a locomotive keeps them + front_section.hold_brake_pipe(5.0); + rear_section.hold_brake_pipe(5.0); + + pair.run(3.0, test::track{0.15, 0.0}); + + CHECK(front_section.speed() == doctest::Approx(rear_section.speed()).epsilon(0.1)); + // rolling resistance takes a little in the 3 s, the collisions used to take most of it + CHECK(0.5 * (front_section.speed() + rear_section.speed()) > 9.0); + CHECK(0.5 * (front_section.speed() + rear_section.speed()) < 10.05 + 1e-6); + CHECK(pair.coupled()); +} + +TEST_CASE("couplers: a collision of coupled vehicles keeps the momentum of the pair") { + // the same jerk through screw couplers, which collide when stretched beyond their travel as well + auto front_wagon{vehicle{definition::load("wagon"), 30.0, "front"}}; + auto rear_wagon{vehicle{definition::load("wagon"), 0.1, "rear"}}; + join(rear_wagon, end::front, front_wagon, end::rear); + test::pair pair{rear_wagon, end::front, front_wagon, end::rear, 0.0}; + REQUIRE(pair.coupled()); + front_wagon.hold_brake_pipe(5.0); + rear_wagon.hold_brake_pipe(5.0); + + pair.run(3.0, test::track{0.15, 0.0}); + + CHECK(0.5 * (front_wagon.speed() + rear_wagon.speed()) > 13.5); + CHECK(0.5 * (front_wagon.speed() + rear_wagon.speed()) < 15.05 + 1e-6); +} From a6778c9fe24f8bee37ce045cf99e797a943da0d0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 6 Nov 2026 19:54:54 +0100 Subject: [PATCH 19/32] refactor: the Sonar remarks on the small fixes taken care of - the log stops through a flag kept in a function, not a global one - the messages are put together with std::format - a vehicle dropped on a trainset in the launcher goes in once the trainset is drawn, so the vehicles are gone through with a plain loop again; the drop split into a new vehicle and a moved one - the scenario that can't be opened is caught as the runtime error it is, a file name outside of the code page as the system error it is - a track of an unknown type is left out through the pointer owning it - the keyword of a track type written out by a function of its own - the picked node forgotten with ranges::replace_if, the empty Forget_Node of the base renderer told why it is empty - FileCased takes the path it works on --- application/application.cpp | 4 +- application/scenarioloadermode.cpp | 7 +- launcher/scenery_list.cpp | 103 +++++++++++++++-------- launcher/scenery_list.h | 4 + launcher/scenery_scanner.cpp | 5 +- rendering/opengl33renderer.cpp | 2 +- rendering/openglrenderer.cpp | 2 +- rendering/renderer.h | 4 +- simulation/simulationstateserializer.cpp | 12 +-- utilities/Logs.cpp | 15 +++- utilities/utilities.cpp | 11 ++- utilities/utilities.h | 2 +- world/Track.cpp | 42 ++++++--- world/Track.h | 2 + 14 files changed, 140 insertions(+), 75 deletions(-) diff --git a/application/application.cpp b/application/application.cpp index 33f5c8a0e..9b7b82853 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -696,8 +696,8 @@ void eu07_application::exit() Global.applicationQuitOrder = true; // each step of the closing in the log, to tell from it where a closing that hangs stops auto const closing{std::chrono::steady_clock::now()}; - auto const step = [&](char const *What) { - WriteLog(std::string{"Closing: "} + What + " at " + std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - closing).count()) + " ms"); + auto const step = [closing](char const *What) { + WriteLog(std::format("Closing: {} at {} ms", What, std::chrono::duration_cast(std::chrono::steady_clock::now() - closing).count())); }; step("the modes"); for (auto &mode : m_modes) diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index eaa034f7e..f8f06ac93 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -21,6 +21,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include "audio/audiorenderer.h" +#include + scenarioloader_mode::scenarioloader_mode() { m_userinterface = std::make_shared(); } @@ -52,8 +54,9 @@ bool scenarioloader_mode::update() { try { state = simulation::State.deserialize_begin(Global.SceneryFile); } - catch (std::exception const &e) { - ErrorLog("Bad init: scenario \"" + Global.SceneryFile + "\" couldn't be opened (" + e.what() + ")"); + // the file it can't read, as the parser and the scenery opening it report it + catch (std::runtime_error const &e) { + ErrorLog(std::format("Bad init: scenario \"{}\" couldn't be opened ({})", Global.SceneryFile, e.what())); state.reset(); Global.SceneryFile.clear(); Application.pop_mode(); diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index 195b0e2c3..6a57db54b 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -8,6 +8,28 @@ #include "utilities/translation.h" #include +#include + +namespace +{ + +// the skin of the bank of vehicles as it's shared, the one a skin dragged out of the bank points at; none for one gone since +std::shared_ptr shared_skin(ui::skin_set const *Skin) +{ + auto const vehicle{Skin->vehicle.lock()}; + if (vehicle == nullptr) + return {}; + auto const found{std::ranges::find_if(vehicle->matching_skinsets, [Skin](std::shared_ptr const &Set) { return Set.get() == Skin; })}; + return found != vehicle->matching_skinsets.end() ? *found : nullptr; +} + +// the name of a vehicle put in a trainset, with a number of its own +std::string numbered(std::string const &Name) +{ + return std::format("{}_{}", Name, static_cast(LocalRandom(0.0, 10000000.0))); +} + +} // namespace ui::scenerylist_panel::scenerylist_panel(scenery_scanner &scanner) : ui_panel(STR("Scenario list"), false), scanner(scanner), placeholder_mini("textures/mini/other") @@ -283,8 +305,8 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) draw_droptarget(trainset, position++); ImGui::SameLine(15.0f); - for (std::size_t index = 0; index < trainset.vehicles.size(); ++index) { - auto &dyn_desc = trainset.vehicles[index]; + // a vehicle dropped between them goes in once they're all drawn, so they're gone through unchanged + for (auto &dyn_desc : trainset.vehicles) { deferred_image *mini = nullptr; if (dyn_desc.skin && dyn_desc.skin->mini.get() != deferred_image::invalid_id) @@ -333,6 +355,10 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) ImGui::PopID(); } + if (m_drop) { + m_drop(); + m_drop = nullptr; + } ImGui::NewLine(); ImGui::PopID(); @@ -341,44 +367,47 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) void ui::scenerylist_panel::draw_droptarget(trainset_desc &trainset, int position) { ImGui::Dummy(ImVec2(15, 30)); - if (ImGui::BeginDragDropTarget()) { - const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("vehicle_pure"); - if (payload) { - skin_set *ptr = *reinterpret_cast(payload->Data); - std::shared_ptr skin; - if (auto const vehicle = ptr->vehicle.lock()) - for (auto &s : vehicle->matching_skinsets) - if (s.get() == ptr) - skin = s; + if (false == ImGui::BeginDragDropTarget()) + return; + if (auto const *payload{ImGui::AcceptDragDropPayload("vehicle_pure")}) + drop_new_vehicle(trainset, position, *static_cast(payload->Data)); + if (auto const *payload{ImGui::AcceptDragDropPayload("vehicle_set")}) + drop_moved_vehicle(trainset, position, *static_cast(payload->Data)); + ImGui::EndDragDropTarget(); +} - if (skin) { - trainset.vehicles.emplace(trainset.vehicles.begin() + position); - dynamic_desc &desc = trainset.vehicles[position]; +// a vehicle of the bank dropped on a gap of a trainset, put in there as a new one of it +void ui::scenerylist_panel::drop_new_vehicle(trainset_desc &trainset, int const position, skin_set const *Skin) +{ + auto skin{shared_skin(Skin)}; + if (skin == nullptr) + return; + m_drop = [&trainset, position, skin] { + auto &desc{*trainset.vehicles.emplace(trainset.vehicles.begin() + position)}; + desc.name = numbered(skin->skin); + desc.vehicle = skin->vehicle.lock(); + desc.skin = skin; + }; +} - desc.name = skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); - desc.vehicle = skin->vehicle.lock(); - desc.skin = skin; - } +// a vehicle of a trainset dropped on a gap of it or of another one: moved there within it, from another one copied in with a +// name of its own +void ui::scenerylist_panel::drop_moved_vehicle(trainset_desc &trainset, int const position, vehicle_moved const &Moved) +{ + auto copy{Moved.dynamic}; + auto &source{Moved.source}; + auto const from{Moved.position}; + if (&trainset != &source) + copy.name = numbered(copy.skin ? copy.skin->skin : copy.name); + m_drop = [&trainset, &source, from, position, copy] { + auto offset{0}; + if (&trainset == &source) { + trainset.vehicles.erase(trainset.vehicles.begin() + from); + if (from < position) + offset = -1; } - - payload = ImGui::AcceptDragDropPayload("vehicle_set"); - if (payload) { - vehicle_moved *moved = reinterpret_cast(payload->Data); - - dynamic_desc desc_copy = moved->dynamic; - int offset = 0; - if (&trainset == &moved->source) { - trainset.vehicles.erase(trainset.vehicles.begin() + moved->position); - if (moved->position < position) - offset = -1; - } else { - desc_copy.name = (desc_copy.skin ? desc_copy.skin->skin : desc_copy.name) + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); - } - - trainset.vehicles.insert(trainset.vehicles.begin() + position + offset, desc_copy); - } - ImGui::EndDragDropTarget(); - } + trainset.vehicles.insert(trainset.vehicles.begin() + position + offset, copy); + }; } void ui::scenerylist_panel::purge_selected_trainset() diff --git a/launcher/scenery_list.h b/launcher/scenery_list.h index 957f1b5ee..0bc91c452 100644 --- a/launcher/scenery_list.h +++ b/launcher/scenery_list.h @@ -54,6 +54,8 @@ private: scenery_desc *selected_scenery = nullptr; trainset_desc *selected_trainset = nullptr; deferred_image placeholder_mini; + // a vehicle dropped on a gap between the vehicles of a trainset: put in there once they're all drawn, not while they are + std::function m_drop; void draw_scenery_list(); void draw_scenery_info(); @@ -67,6 +69,8 @@ private: void open_link(const std::string &link); void draw_trainset(trainset_desc &trainset); void draw_droptarget(trainset_desc &trainset, int position); + void drop_new_vehicle(trainset_desc &trainset, int position, skin_set const *Skin); + void drop_moved_vehicle(trainset_desc &trainset, int position, vehicle_moved const &Moved); void purge_selected_trainset(); }; } // namespace ui diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index 5f6a6d2f9..6e258425e 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -2,6 +2,8 @@ #include "scenery_scanner.h" #include "utilities/Logs.h" +#include + scenery_scanner::scenery_scanner(ui::vehicles_bank &bank) : bank(bank) { @@ -17,7 +19,8 @@ void scenery_scanner::scan() try { name = path.string(); } - catch (std::exception const &) { + catch (std::system_error const &) { + // a name outside of the code page, which the path can't give as a string on Windows continue; } diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index a5d1f3894..a8a1b2139 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -5414,7 +5414,7 @@ void opengl33_renderer::Forget_Node(scene::basic_node const *Node) { if (m_picksceneryitem == Node) m_picksceneryitem = nullptr; - std::replace_if(m_picksceneryitems.begin(), m_picksceneryitems.end(), [Node](scene::basic_node const *Item) { return Item == Node; }, nullptr); + std::ranges::replace_if(m_picksceneryitems, [Node](scene::basic_node const *Item) { return Item == Node; }, nullptr); } glm::dvec3 opengl33_renderer::Update_Mouse_Position() diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 864d84141..6313aa55f 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -1894,7 +1894,7 @@ opengl_renderer::Forget_Node( scene::basic_node const *Node ) { if( m_picksceneryitem == Node ) { m_picksceneryitem = nullptr; } - std::replace_if( m_picksceneryitems.begin(), m_picksceneryitems.end(), [Node]( scene::basic_node const *Item ) { return Item == Node; }, nullptr ); + std::ranges::replace_if( m_picksceneryitems, [Node]( scene::basic_node const *Item ) { return Item == Node; }, nullptr ); } void diff --git a/rendering/renderer.h b/rendering/renderer.h index e9dc8a47c..5850e1754 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -91,7 +91,9 @@ public: virtual auto Pick_Control() const -> TSubModel const * = 0; virtual auto Pick_Node() const -> scene::basic_node const * = 0; // a node deleted from the scene is no longer given as picked - virtual void Forget_Node( scene::basic_node const *Node ) {} + virtual void Forget_Node( scene::basic_node const * /*Node*/ ) { + // a renderer which keeps no node picked has none to forget + } virtual auto Mouse_Position() const -> glm::dvec3 = 0; virtual auto Mouse_Hit() const -> bool { return true; } diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 78955c828..664f3b955 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "editor/editorTerrainStreamer.hpp" +#include namespace simulation { @@ -542,13 +543,14 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } else if( nodedata.type == "track" ) { - auto *path { deserialize_path( Input, Scratchpad, nodedata ) }; - if( path->eType == tt_Unknown ) { - // with no path made it can't be joined nor drawn, and nothing could run on it - ErrorLog( "Bad scenario: track \"" + path->name() + "\" of an unknown type defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ") left out" ); - delete path; + std::unique_ptr made { deserialize_path( Input, Scratchpad, nodedata ) }; + if( made->eType == tt_Unknown ) { + // with no path made it can't be joined nor drawn, and nothing could run on it: it goes with its holder + ErrorLog( std::format( "Bad scenario: track \"{}\" of an unknown type defined in file \"{}\" (line {}) left out", made->name(), Input.Name(), inputline ) ); return; } + // from here on the scene owns it + auto *path { made.release() }; // duplicates of named tracks are currently experimentally allowed if( false == simulation::Paths.insert( path ) ) { ErrorLog( "Bad scenario: duplicate track name \"" + path->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); diff --git a/utilities/Logs.cpp b/utilities/Logs.cpp index 18020d6f1..38094d72e 100644 --- a/utilities/Logs.cpp +++ b/utilities/Logs.cpp @@ -73,12 +73,19 @@ std::deque ErrorStack; // lock for log stacks std::mutex logMutex; -// the log is written through the closing of the simulator, to the end -std::atomic logStop{false}; +namespace +{ +// the log is written through the closing of the simulator, to the end: set once the closing has nothing more to tell +std::atomic &log_stopped() +{ + static std::atomic stopped{false}; + return stopped; +} +} // namespace void StopLogService() { - logStop = true; + log_stopped() = true; } @@ -87,7 +94,7 @@ void LogService() // one more pass after the stop, so what came just before it gets written too for (bool last{false}; false == last;) { - last = logStop; + last = log_stopped(); { // --- Obsługa InfoStack --- while (!InfoStack.empty()) diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index b25c6b31d..cf9003f4d 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -471,23 +471,22 @@ bool FileExists(std::string const &Filename) return std::filesystem::exists(Filename); } -std::string FileCased(std::string const &Filename) +std::string FileCased(std::filesystem::path const &Filename) { std::error_code error; if (std::filesystem::exists(Filename, error)) - return Filename; + return Filename.string(); #ifdef _WIN32 // the names there are found in any case already: no folder to go through for every file missing, nor names outside of the // code page to throw on return {}; #else // only the name of the file itself, a folder named in another case isn't found - std::filesystem::path const path{Filename}; - auto const directory{path.has_parent_path() ? path.parent_path() : std::filesystem::path{"."}}; - auto const wanted{ToLower(path.filename().string())}; + auto const directory{Filename.has_parent_path() ? Filename.parent_path() : std::filesystem::path{"."}}; + auto const wanted{ToLower(Filename.filename().string())}; for (std::filesystem::directory_iterator entry{directory, error}, end; entry != end; entry.increment(error)) if (ToLower(entry->path().filename().string()) == wanted) - return path.has_parent_path() ? (directory / entry->path().filename()).string() : entry->path().filename().string(); + return Filename.has_parent_path() ? (directory / entry->path().filename()).string() : entry->path().filename().string(); return {}; #endif } diff --git a/utilities/utilities.h b/utilities/utilities.h index f16431dde..cc4056cae 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -219,7 +219,7 @@ template <> bool extract_value(bool &Variable, std::string const &Key, std::stri bool FileExists(std::string const &Filename); // the file as it's named on the disk when only the case of its name differs, as in the sceneries made on Windows; empty when // there's none. the folders have to be named in their case; on Windows, finding the names in any case itself, the file given or none -std::string FileCased(std::string const &Filename); +std::string FileCased(std::filesystem::path const &Filename); std::pair FileExists(std::vector const &Names, std::vector const &Extensions); diff --git a/world/Track.cpp b/world/Track.cpp index 14998ce7c..c6339a424 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -29,6 +29,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "rendering/renderer.h" +#include + // 101206 Ra: trapezoidalne drogi i tory // 110720 Ra: rozprucie zwrotnicy i odcinki izolowane @@ -429,7 +431,7 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) { // dopływ: no path of it was ever made, the simulator falling over it as it loaded. a river of its first path now, the // second one kept and written back - ErrorLog("Bad track: tributary \"" + m_name + "\" in file \"" + parser->Name() + "\" (line " + std::to_string(parser->Line()) + ") is drawn and run on as a river of its first path"); + ErrorLog(std::format("Bad track: tributary \"{}\" in file \"{}\" (line {}) is drawn and run on as a river of its first path", m_name, parser->Name(), parser->Line())); eType = tt_Normal; iCategoryFlag = 4; m_tributary = true; @@ -2319,25 +2321,37 @@ TTrack::deserialize_( std::istream &Input ) { // TODO: implement } +// the keyword the track is written with, by its type and for a plain one by what runs on it +char const * +TTrack::type_keyword() const { + + if( eType == tt_Normal && m_tributary ) { + return "tributary"; + } + if( eType == tt_Normal ) { + switch( iCategoryFlag ) { + case 1: { return "normal"; } + case 2: { return "road"; } + case 4: { return "river"; } + default: { return "none"; } + } + } + switch( eType ) { + case tt_Switch: { return "switch"; } + case tt_Cross: { return "cross"; } + case tt_Table: { return "turn"; } + case tt_Tributary: { return "tributary"; } + default: { return "none"; } + } +} + // export() subclass details, sends basic content of the class in legacy (text) format to provided stream void TTrack::export_as_text_( std::ostream &Output ) const { // header Output << "track "; // type - Output << ( - eType == tt_Normal ? ( - m_tributary ? "tributary" : - iCategoryFlag == 1 ? "normal" : - iCategoryFlag == 2 ? "road" : - iCategoryFlag == 4 ? "river" : - "none" ) : - eType == tt_Switch ? "switch" : - eType == tt_Cross ? "cross" : - eType == tt_Table ? "turn" : - eType == tt_Tributary ? "tributary" : - "none" ) - << ' '; + Output << type_keyword() << ' '; // basic attributes Output << Length() << ' ' diff --git a/world/Track.h b/world/Track.h index 5952da6a7..70431072a 100644 --- a/world/Track.h +++ b/world/Track.h @@ -358,6 +358,8 @@ private: void deserialize_( std::istream &Input ); // export() subclass details, sends basic content of the class in legacy (text) format to provided stream void export_as_text_( std::ostream &Output ) const; + // the keyword the track is written with, by its type and for a plain one by what runs on it + char const *type_keyword() const; // locates specified profile in the profile database, potentially loading it from a file static std::pair fetch_track_rail_profile( std::string const &Profile ); // loads content of specified file and converts it into a vertex array From d232cd0a6937dda6bb482fac2a9b23e10bdd7fb7 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 6 Nov 2026 19:58:24 +0100 Subject: [PATCH 20/32] refactor: the Sonar remarks on the material copies taken care of - the params of a shader named by the type the shader keeps them in - ranges::any_of and erase_if, with explicit captures - the weathering set component by component with an index of the type of the vector - the lookups declared in the ifs that use them - the empty Erase of the base renderer told why it is empty --- model/material.cpp | 51 ++++++++++++++++++++++---------------------- rendering/renderer.h | 4 +++- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/model/material.cpp b/model/material.cpp index 2b92986d0..4e96ddd8c 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -684,17 +684,20 @@ variant_of( opengl_material const &Source, std::string const &Suffix ) { return variant; } +// the params of a shader, as the shader keeps them +using param_confs = decltype( gl::shader::param_conf ); + // whether another param of the shader lies on a component of the named one, which a value set for that one would change too bool -shares_slot( std::unordered_map const &Conf, std::string const &Name ) { +shares_slot( param_confs const &Conf, std::string const &Name ) { auto const own { Conf.find( Name ) }; if( own == Conf.end() ) { return false; } - return std::any_of( - std::begin( Conf ), std::end( Conf ), - [&]( auto const &Entry ) { + return std::ranges::any_of( + Conf, + [ &Name, &own ]( auto const &Entry ) { return Entry.first != Name && Entry.second.location == own->second.location && Entry.second.offset < own->second.offset + own->second.size @@ -715,15 +718,16 @@ make_matte( opengl_material &Variant ) { || shares_slot( Variant.shader->param_conf, key ) ) { continue; } - for( std::size_t i = 0; i < entry->second.size; ++i ) { - Variant.params[ entry->second.location ][ entry->second.offset + i ] = value[ static_cast( i ) ]; + auto &slot { Variant.params[ entry->second.location ] }; + auto const offset { static_cast( entry->second.offset ) }; + auto const size { static_cast( entry->second.size ) }; + for( glm::length_t i = 0; i < size; ++i ) { + slot[ offset + i ] = value[ i ]; } - Variant.params_state.erase( - std::remove_if( - std::begin( Variant.params_state ), std::end( Variant.params_state ), - [&]( gl::shader::param_entry const &State ) { - return State.location == entry->second.location && State.offset == entry->second.offset; } ), - std::end( Variant.params_state ) ); + std::erase_if( + Variant.params_state, + [ &entry ]( gl::shader::param_entry const &State ) { + return State.location == entry->second.location && State.offset == entry->second.offset; } ); } } @@ -737,26 +741,24 @@ make_tinted( opengl_material &Variant, glm::vec3 const &Colour ) { } auto const &conf { Variant.shader->param_conf }; auto const weathering { conf.find( "weathering" ) }; - auto const tint { conf.find( "tint" ) }; - if( weathering == conf.end() + if( auto const tint { conf.find( "tint" ) }; + weathering == conf.end() || tint == conf.end() || weathering->second.offset != 3 || tint->second.location != weathering->second.location || tint->second.offset != 0 - || std::any_of( - std::begin( conf ), std::end( conf ), - [&]( auto const &Entry ) { + || std::ranges::any_of( + conf, + [ &weathering, &tint ]( auto const &Entry ) { return Entry.first != weathering->first && Entry.first != tint->first && Entry.second.location == weathering->second.location; } ) ) { return false; } auto const location { weathering->second.location }; Variant.params[ location ] = glm::vec4{ Colour, -1.f }; - Variant.params_state.erase( - std::remove_if( - std::begin( Variant.params_state ), std::end( Variant.params_state ), - [&]( gl::shader::param_entry const &State ) { - return State.location == location; } ), - std::end( Variant.params_state ) ); + std::erase_if( + Variant.params_state, + [ location ]( gl::shader::param_entry const &State ) { + return State.location == location; } ); return true; } @@ -768,8 +770,7 @@ material_manager::create_matte( material_handle const Material ) { if( Material == null_handle || Material >= m_materials.size() ) { return Material; } - auto const lookup { m_mattes.find( Material ) }; - if( lookup != m_mattes.end() ) { + if( auto const lookup { m_mattes.find( Material ) }; lookup != m_mattes.end() ) { return lookup->second; } auto matte { variant_of( m_materials[ Material ], ":matte" ) }; diff --git a/rendering/renderer.h b/rendering/renderer.h index 4dabc204a..1d60a1d95 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -60,7 +60,9 @@ public: // adds supplied vertex data at the end of specified chunk virtual auto Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) -> bool = 0; // empties specified chunk, its handle stays valid to be filled again - virtual void Erase( gfx::geometry_handle const &Geometry ) {} + virtual void Erase( gfx::geometry_handle const & /*Geometry*/ ) { + // a renderer which keeps no chunks of its own has none to empty + } // provides direct access to index data of specfied chunk virtual auto Indices( gfx::geometry_handle const &Geometry ) const->gfx::index_array const & = 0; // provides direct access to vertex data of specfied chunk From e0cef91049c3089c0ac0a2bb93a78d2dc53595ba Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sat, 7 Nov 2026 04:48:15 +0100 Subject: [PATCH 21/32] fix(physics): running backwards, the camshaft goes back down from the last series position (eu07/maszyna#393) (#256) Running backwards, AutoRelayCheck() keeps the camshaft from joining the motors in more branches than BackwardsBranchesAllowed (the series connection alone, by default). It did that by returning before any move of the camshaft, so once it reached the last series position (22 of 43 in the Skoda 181 and 182, the next one joins the motors in two branches) it stayed there whatever the driver did with the master controller: with the controller back at zero the line contactors stayed closed and the motors kept pulling, until the line contactors were opened by hand. Now the limit only keeps the camshaft from going further up; it goes back down as usual. Tests: tests/unit/test_camshaft.cpp, on a locomotive made after the Skoda 181 (its RList); the test definitions take the RList and MotorParamTable0 tables. --- McZapkie/Mover.cpp | 11 ++- tests/unit/test_camshaft.cpp | 135 +++++++++++++++++++++++++++++++++++ tests/unit/vehicle.cpp | 2 + 3 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_camshaft.cpp diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 954e277ba..6b7bdcec5 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -6904,13 +6904,12 @@ bool TMoverParameters::AutoRelayCheck(void) } else { // zmieniaj mainctrlactualpos - if (DirActive < 0 && TrainType != dt_PseudoDiesel && RList[MainCtrlActualPos + 1].Bn > BackwardsBranchesAllowed) - { - return false; // nie poprawiamy przy konwersji - // return ARC;// bbylo exit; //Ra: to powoduje, że EN57 nie wyłącza się przy IminLo - } + // running backwards, the motors don't get joined in more branches than allowed (the series connection + // alone, usually); the camshaft still goes back down from there (eu07/maszyna#393: it stayed where it + // was, the line contactors closed, whatever the driver did with the master controller) + auto const backwardslimit{DirActive < 0 && TrainType != dt_PseudoDiesel && RList[MainCtrlActualPos + 1].Bn > BackwardsBranchesAllowed}; // main bez samoczynnego rozruchu - if (MainCtrlActualPos < std::size(RList) - 1 // crude guard against running out of current fixed table + if (false == backwardslimit && MainCtrlActualPos < std::size(RList) - 1 // crude guard against running out of current fixed table && (RList[MainCtrlActualPos].Relay < MainCtrlPos || (RList[MainCtrlActualPos + 1].Relay == MainCtrlPos && MainCtrlActualPos < RlistSize) || (TrainType == dt_ET22 && DelayCtrlFlag))) { diff --git a/tests/unit/test_camshaft.cpp b/tests/unit/test_camshaft.cpp new file mode 100644 index 000000000..940616a62 --- /dev/null +++ b/tests/unit/test_camshaft.cpp @@ -0,0 +1,135 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +using test::definition; +using test::vehicle; + +// eu07/maszyna#393: running backwards, a camshaft which reached the last position of the series connection (the +// next one already joins the motors in more branches than BackwardsBranchesAllowed) stayed there whatever the driver +// did with the master controller, with the line contactors closed and the motors pulling, until the line contactors +// were opened by hand + +namespace { + +// the starting resistances of the Skoda 181 (cd/181_v2): 6 motors in series up to position 22, then two branches of +// 3 up to 34, then three branches of 2 up to 43; columns: position, resistance, branches, motors in a branch, auto +nlohmann::ordered_json skoda_rlist() { + nlohmann::ordered_json rows{nlohmann::ordered_json::array({0, 0, 0, 0, 0}), nlohmann::ordered_json::array({1, 999999, 1, 6, 0})}; + auto const series{std::vector{15.87, 12.32, 10.03, 8.54, 6.98, 5.84, 5.35, 4.87, 3.88, 2.92, 2.68, 2.43, 2.19, 1.95, 1.70, 1.46, 1.22, 0.97, 0.49, 0.24, 0.00}}; + auto const seriesparallel{std::vector{4.50, 3.63, 3.11, 2.42, 2.08, 1.73, 1.38, 1.04, 0.69, 0.35, 0.17, 0.00}}; + auto const parallel{std::vector{2.78, 2.29, 1.80, 1.41, 1.02, 0.73, 0.44, 0.24, 0.00}}; + auto position{2}; + for (auto const resistance : series) { + rows.push_back(nlohmann::ordered_json::array({position++, resistance, 1, 6, 0})); + } + for (auto const resistance : seriesparallel) { + rows.push_back(nlohmann::ordered_json::array({position++, resistance, 2, 3, 0})); + } + for (auto const resistance : parallel) { + rows.push_back(nlohmann::ordered_json::array({position++, resistance, 3, 2, 0})); + } + return rows; +} + +// an electric locomotive with a camshaft, made after the Skoda 181 +definition skoda() { + auto result{definition::load("wagon") + .set("Param", "M", 124200) + .set("Param", "Vmax", 90) + .set("Dimensions", "L", 18.8) + .set("Wheels", "D", 1.25) + .set("Wheels", "Axle", "Co'Co'") + .set("Engine", "EngineType", "ElectricSeriesMotor") + .set("Engine", "Volt", 967) + .set("Engine", "Trans", "21:89") + .set("Engine", "WindingRes", 0.084) + .set("Engine", "nmax", 1870) + .set("Circuit", "CircuitRes", 0.01) + .set("Circuit", "ImaxLo", 550) + .set("Circuit", "ImaxHi", 550) + .set("Circuit", "IminLo", 307) + .set("Circuit", "IminHi", 332) + .set("Cntrl", "MCPN", 43) + .set("Cntrl", "SCPN", 3) + .set("Cntrl", "CoupledCtrl", "No") + .set("Cntrl", "IniCDelay", 0.2) + .set("Cntrl", "SCDelay", 0.18) + .set("Cntrl", "SCDDelay", 0.18) + .set("Cntrl", "Camshaft", "Yes") + .set("MotorParamTable0", "rows", + nlohmann::ordered_json::array({nlohmann::ordered_json::array({0, 33.75, 57, 0.4, 225.70, 57, 0.4}), nlohmann::ordered_json::array({1, 31.66, 69, 0.4, 217.10, 69, 0.4}), + nlohmann::ordered_json::array({2, 30.52, 90, 0.4, 212.80, 90, 0.4}), nlohmann::ordered_json::array({3, 28.74, 107, 0.4, 199.55, 107, 0.4})})) + .set("RList", "Size", 43) + .set("RList", "rows", skoda_rlist())}; + return result; +} + +// the locomotive running with the line contactors closed and the camshaft at Position, the master controller moved +// to Controller, in the given direction; the time since the last step of the camshaft long enough for the next one +void running(TMoverParameters &Locomotive, int const Direction, int const Position, int const Controller) { + Locomotive.CabActive = 1; + Locomotive.DirActive = Direction; + Locomotive.Mains = true; + Locomotive.PipePress = 5.0; // bar, the pressure switch lets the line contactors close + Locomotive.BrakePress = 0.0; + Locomotive.StLinFlag = true; + Locomotive.MainCtrlActualPos = Position; + Locomotive.MainCtrlPos = Controller; + Locomotive.LastRelayTime = 10.0; +} + +// steps the camshaft as the physics does, each step once the delay since the previous one passed; returns where it +// ended up +int run_camshaft(TMoverParameters &Locomotive, int const Steps) { + for (int step = 0; step < Steps; ++step) { + Locomotive.LastRelayTime = 10.0; + Locomotive.AutoRelayCheck(); + } + return Locomotive.MainCtrlActualPos; +} + +} // namespace + +TEST_CASE("camshaft: running backwards, the camshaft goes back down from the last series position (eu07/maszyna#393)") { + vehicle locomotive{skoda()}; + auto &mover{locomotive.mover()}; + REQUIRE(mover.HasCamshaft); + REQUIRE(mover.RlistSize == 43); + REQUIRE(mover.RList[22].Bn == 1); + REQUIRE(mover.RList[23].Bn == 2); + + SUBCASE("the controller turned back a few positions") { + running(mover, -1, 22, 15); + CHECK(run_camshaft(mover, 1) == 21); // it used to stay at 22 + CHECK(run_camshaft(mover, 10) == 15); + } + SUBCASE("the controller turned back to zero: the camshaft runs down and the line contactors open") { + running(mover, -1, 22, 0); + run_camshaft(mover, 30); + CHECK(mover.MainCtrlActualPos == 0); + CHECK_FALSE(mover.StLinFlag); // the motors don't pull any more + } +} + +TEST_CASE("camshaft: running backwards, the camshaft still doesn't go past the series connection") { + vehicle locomotive{skoda()}; + auto &mover{locomotive.mover()}; + running(mover, -1, 20, 43); + CHECK(run_camshaft(mover, 10) == 22); + + SUBCASE("unless more branches are allowed backwards") { + mover.BackwardsBranchesAllowed = 2; + CHECK(run_camshaft(mover, 20) == 34); + } +} + +TEST_CASE("camshaft: running forwards, the camshaft goes through all the connections and back") { + vehicle locomotive{skoda()}; + auto &mover{locomotive.mover()}; + running(mover, 1, 20, 43); + CHECK(run_camshaft(mover, 30) == 43); + + mover.MainCtrlPos = 10; + CHECK(run_camshaft(mover, 40) == 10); +} diff --git a/tests/unit/vehicle.cpp b/tests/unit/vehicle.cpp index c0930d134..914a31079 100644 --- a/tests/unit/vehicle.cpp +++ b/tests/unit/vehicle.cpp @@ -45,6 +45,8 @@ std::map> const sections{ {"DimmerList", {':', "endDimmerList"}}, {"WiperList", {':', "endwl"}}, {"CompressorList", {':', "endCL"}}, + {"RList", {':', "END-RL"}}, + {"MotorParamTable0", {':', "END-MPT"}}, }; section_format const &format_of(std::string const &Section) { From 28cf5fe276ccc61087da9d1800b9c5738094af6f Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 15 Oct 2026 22:26:13 +0200 Subject: [PATCH 22/32] fix(gfx): a vehicle taken off the tracks stops smoking (#90) The smoke sources of a vehicle stay in the particle manager when the vehicle is taken off the tracks: one gone through a portal, or a road vehicle waiting in the pool of its spawn point. They kept spawning particles where the vehicle had vanished, for good. A source of a disabled vehicle spawns no more particles now, the ones it let out fade away as usual, and a pooled road vehicle puffs again once it's back on the road. Checked on td.scn with a portal added at the dead end of tdo_n585 and an SM42 driven into it by the AI: on master its smoke source kept about 100 particles where it vanished for the rest of the run (over 200 s); with the fix they were gone within a second. --- rendering/particles.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rendering/particles.cpp b/rendering/particles.cpp index 5f7158e0c..6719eb6df 100644 --- a/rendering/particles.cpp +++ b/rendering/particles.cpp @@ -162,8 +162,15 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { glm::dvec3{ std::numeric_limits::max() }, glm::dvec3{ std::numeric_limits::lowest() } }; + // a vehicle taken off the tracks (gone through a portal, or waiting in the pool of the road traffic) puffs no more, + // the particles it already let out fade away as usual + auto const nospawn { + false == Global.Smoke + || true == Onlydespawn + || ( m_ownertype == owner_type::vehicle && false == m_owner.vehicle->bEnabled ) }; + m_spawncount = ( - ( ( false == Global.Smoke ) || ( true == Onlydespawn ) ) ? + ( true == nospawn ) ? 0.f : std::min( static_cast(m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity )), @@ -179,7 +186,7 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { if (m_owner.vehicle->MoverParameters->dizel_spinup == true) { m_spawncount = - ((false == Global.Smoke) || (true == Onlydespawn)) ? + (true == nospawn) ? 0.f : std::min( // m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity ), @@ -191,7 +198,7 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { m_owner.vehicle->MoverParameters->Im == 0) { m_spawncount = - ((false == Global.Smoke) || (true == Onlydespawn)) ? + (true == nospawn) ? 0.f : std::min( // m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity ), @@ -201,7 +208,7 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { { m_spawncount = - ((false == Global.Smoke) || (true == Onlydespawn)) ? + (true == nospawn) ? 0.f : std::min( // m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity ), From f94b3080629523d991683fabff7f6ec3584534d8 Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Thu, 15 Oct 2026 22:26:13 +0200 Subject: [PATCH 23/32] fix(sound): check for the player train in the outer noise test of a vehicle The test whether a vehicle belongs to the player train dereferenced simulation::Train without a null check (commented out long ago). The driver mode switches to the free camera in the same frame the player train goes through a portal, before the sounds are updated, so it isn't known to crash, but nothing else guards it. --- vehicle/DynObj.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index c6793e454..6f151f0e8 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -4863,7 +4863,7 @@ void TDynamicObject::RenderSounds() { #endif && ( // compound test whether the vehicle belongs to user-driven consist (as these don't emit outer noise in cab view) FreeFlyModeFlag ? true : // in external view all vehicles emit outer noise - // Global.pWorld->train() == nullptr ? true : // (can skip this check, with no player train the external view is a given) + simulation::Train == nullptr ? true : // no player train, e.g. it went through a portal and the view isn't switched yet ctOwner == nullptr ? true : // standalone vehicle, can't be part of user-driven train ctOwner != simulation::Train->Dynamic()->ctOwner ? true : // confirmed isn't a part of the user-driven train Global.CabWindowOpen ? true : // sticking head out we get to hear outer noise From 4489dd7469e16437a125d1bc399d7127f54237dc Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sat, 7 Nov 2026 11:38:49 +0100 Subject: [PATCH 24/32] test(gfx): unit tests of the smoke of a vehicle taken off the tracks (#90) The spawn count of the smoke source of a vehicle moves out of smoke_source::update() into smoke::vehicle_spawn_count() (vehicle/smoke.h), a free function on the mover state, which joins the core library so the unit tests can call it the way the source does. The rates are the same as before, the steady rate of the source still counted in before the share of the engine. Tests: tests/unit/test_smoke.cpp. A vehicle taken off the tracks spawns no smoke and drops the pending particles, with the engine starting, idling or under load; a road vehicle back from the pool puffs again; the engine adds to the steady rate; the pending count stays within the room of the source. Two of the four fail without the fix. Checked again on td.scn with the SM42 driven into a portal: the smoke source keeps puffing on the run and drops to 0 particles once the vehicle is taken off the tracks. --- CMakeLists.txt | 1 + rendering/particles.cpp | 75 +++++++++++----------------------- tests/unit/test_smoke.cpp | 85 +++++++++++++++++++++++++++++++++++++++ vehicle/smoke.cpp | 46 +++++++++++++++++++++ vehicle/smoke.h | 30 ++++++++++++++ 5 files changed, 186 insertions(+), 51 deletions(-) create mode 100644 tests/unit/test_smoke.cpp create mode 100644 vehicle/smoke.cpp create mode 100644 vehicle/smoke.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dfe58b66d..9d19e844c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -463,6 +463,7 @@ set(CORE_SOURCES "scripting/ladderlogic.cpp" "vehicle/handover.cpp" "vehicle/horn.cpp" +"vehicle/smoke.cpp" ) add_library(eu07_core STATIC ${CORE_SOURCES}) target_link_libraries(eu07_core PUBLIC eu07_settings) diff --git a/rendering/particles.cpp b/rendering/particles.cpp index 6719eb6df..8ccb728dd 100644 --- a/rendering/particles.cpp +++ b/rendering/particles.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "simulation/simulationenvironment.h" #include "utilities/Logs.h" +#include "vehicle/smoke.h" void @@ -162,60 +163,32 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { glm::dvec3{ std::numeric_limits::max() }, glm::dvec3{ std::numeric_limits::lowest() } }; - // a vehicle taken off the tracks (gone through a portal, or waiting in the pool of the road traffic) puffs no more, - // the particles it already let out fade away as usual - auto const nospawn { - false == Global.Smoke - || true == Onlydespawn - || ( m_ownertype == owner_type::vehicle && false == m_owner.vehicle->bEnabled ) }; + auto const spawning { ( true == Global.Smoke ) && ( false == Onlydespawn ) }; - m_spawncount = ( - ( true == nospawn ) ? - 0.f : - std::min( - static_cast(m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity )), - static_cast(m_max_particles) ) ); - // consider special spawn rate cases if( m_ownertype == owner_type::vehicle ) { - if (Global.AirTemperature <= 5.f || m_owner.vehicle->MoverParameters->dizel_heat.Ts < 45.f) - { - m_emitter.color = glm::vec3{128, 128, 128}; - } - - if (m_owner.vehicle->MoverParameters->dizel_spinup == true) - { - m_spawncount = - (true == nospawn) ? - 0.f : - std::min( - // m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity ), - static_cast(m_spawncount +(m_spawnrate * Timedelta * Global.SmokeFidelity * (((m_owner.vehicle->MoverParameters->enrot) / 4 ) * 0.01))), static_cast(m_max_particles)); - } - else - { - if (m_owner.vehicle->MoverParameters->DirAbsolute == 0 || - m_owner.vehicle->MoverParameters->Im == 0) - { - m_spawncount = - (true == nospawn) ? - 0.f : - std::min( - // m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity ), - static_cast(m_spawncount + (m_spawnrate * Timedelta * Global.SmokeFidelity * ((((m_owner.vehicle->MoverParameters->DElist[m_owner.vehicle->MoverParameters->MainCtrlPosNo].RPM -m_owner.vehicle->MoverParameters->enrot) /60) *0.02) *(m_owner.vehicle->MoverParameters->EnginePower * 0.005)))), static_cast(m_max_particles)); - } - else - { - - m_spawncount = - (true == nospawn) ? - 0.f : - std::min( - // m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity ), - static_cast(m_spawncount + (m_spawnrate * Timedelta * Global.SmokeFidelity * ((((m_owner.vehicle->MoverParameters->DElist[m_owner.vehicle->MoverParameters->MainCtrlPosNo].RPM -m_owner.vehicle->MoverParameters->enrot) /60) * (sqrt(m_owner.vehicle->MoverParameters->Im) * 0.01) * 0.02) *(m_owner.vehicle->MoverParameters->EnginePower * 0.005)))), static_cast(m_max_particles)); - } - } - } + auto const &mover { *m_owner.vehicle->MoverParameters }; + if( Global.AirTemperature <= 5.f || mover.dizel_heat.Ts < 45.f ) { + m_emitter.color = glm::vec3{ 128, 128, 128 }; + } + // a vehicle taken off the tracks (gone through a portal, or waiting in the pool of the road traffic) puffs no more, + // the particles it already let out fade away as usual + m_spawncount = ( + ( true == spawning ) ? + smoke::vehicle_spawn_count( + mover, m_owner.vehicle->bEnabled, + { m_spawncount, m_spawnrate, Global.SmokeFidelity, static_cast( m_max_particles ) }, + Timedelta ) : + 0.f ); + } + else { + m_spawncount = ( + ( true == spawning ) ? + std::min( + static_cast(m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity )), + static_cast(m_max_particles) ) : + 0.f ); + } // update spawned particles for( auto particleiterator { std::begin( m_particles ) }; particleiterator != std::end( m_particles ); ++particleiterator ) { diff --git a/tests/unit/test_smoke.cpp b/tests/unit/test_smoke.cpp new file mode 100644 index 000000000..05fb8ae8b --- /dev/null +++ b/tests/unit/test_smoke.cpp @@ -0,0 +1,85 @@ +#include "stdafx.h" +#include "doctest.h" + +#include "vehicle.h" + +#include "vehicle/smoke.h" + +// the smoke of a vehicle's engine: how many particles the smoke source of the vehicle's model spawns in an update + +namespace { + +// a source of 20 particles per second, with room for 100 +smoke::spawn_rate const source{0.f, 20.f, 1.f, 100.f}; +double const update{0.1}; // s + +// a diesel at work, its engine in the state of one of the cases below +test::vehicle diesel() { + test::vehicle vehicle{test::definition::load("wagon")}; + vehicle->MainCtrlPosNo = 1; + vehicle->DElist[1].RPM = 1500.0; + vehicle->enrot = 300.0; + vehicle->EnginePower = 400.0; + return vehicle; +} + +} // namespace + +TEST_CASE("smoke: a vehicle taken off the tracks puffs no more (#90)") { + auto vehicle{diesel()}; + SUBCASE("engine starting") { vehicle->dizel_spinup = true; } + SUBCASE("engine idling") { vehicle->DirAbsolute = 0; } + SUBCASE("engine under load") { + vehicle->DirAbsolute = 1; + vehicle->Im = 400.0; + } + // on the tracks the engine puffs + REQUIRE(smoke::vehicle_spawn_count(vehicle.mover(), true, source, update) >= 1.f); + + // gone through a portal, the pending particles are dropped too + auto pending{source}; + pending.pending = 5.f; + CHECK(smoke::vehicle_spawn_count(vehicle.mover(), false, pending, update) == 0.f); +} + +TEST_CASE("smoke: a road vehicle back on the road from the pool puffs again") { + auto vehicle{diesel()}; + // waiting in the pool, the count stays at zero update after update + auto rate{source}; + for (auto i{0}; i < 10; ++i) { + rate.pending = smoke::vehicle_spawn_count(vehicle.mover(), false, rate, update); + } + REQUIRE(rate.pending == 0.f); + + CHECK(smoke::vehicle_spawn_count(vehicle.mover(), true, rate, update) >= 1.f); +} + +TEST_CASE("smoke: the engine adds to the steady rate of the source") { + auto vehicle{diesel()}; + vehicle->DirAbsolute = 0; + auto const steady{source.rate * update}; // the rate of a source of the scenery + + vehicle->EnginePower = 0.0; + CHECK(smoke::vehicle_spawn_count(vehicle.mover(), true, source, update) == doctest::Approx(steady)); + + vehicle->EnginePower = 400.0; + auto const working{smoke::vehicle_spawn_count(vehicle.mover(), true, source, update)}; + CHECK(working > steady); + + // the faster the starting engine turns, the more it puffs + vehicle->dizel_spinup = true; + vehicle->enrot = 100.0; + auto const slow{smoke::vehicle_spawn_count(vehicle.mover(), true, source, update)}; + vehicle->enrot = 400.0; + CHECK(smoke::vehicle_spawn_count(vehicle.mover(), true, source, update) > slow); +} + +TEST_CASE("smoke: the pending particles never outgrow the room of the source") { + auto vehicle{diesel()}; + vehicle->dizel_spinup = true; + vehicle->enrot = 100000.0; + auto rate{source}; + rate.pending = 99.f; + + CHECK(smoke::vehicle_spawn_count(vehicle.mover(), true, rate, 10.0) == rate.limit); +} diff --git a/vehicle/smoke.cpp b/vehicle/smoke.cpp new file mode 100644 index 000000000..b6e923222 --- /dev/null +++ b/vehicle/smoke.cpp @@ -0,0 +1,46 @@ +/* +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 "vehicle/smoke.h" + +#include "McZapkie/MOVER.h" + +namespace smoke { + +namespace { + +// how hard the engine works, scaling the spawn rate of the source +double engine_load(TMoverParameters const &Mover) { + if (Mover.dizel_spinup) { + // starting up, it puffs harder the faster it turns + return ((Mover.enrot / 4) * 0.01); + } + auto const rpmrise{(Mover.DElist[Mover.MainCtrlPosNo].RPM - Mover.enrot) / 60}; + if (Mover.DirAbsolute == 0 || Mover.Im == 0) { + // idling: speeding up to the revolutions of the master controller + return ((rpmrise * 0.02) * (Mover.EnginePower * 0.005)); + } + // under load: the more current the traction motors draw, the more smoke + return ((rpmrise * (std::sqrt(Mover.Im) * 0.01) * 0.02) * (Mover.EnginePower * 0.005)); +} + +} // namespace + +float vehicle_spawn_count(TMoverParameters const &Mover, bool const Ontracks, spawn_rate const &Rate, double const Timedelta) { + if (false == Ontracks) { + return 0.f; + } + // the steady rate of the source, the way a source of the scenery spawns... + auto const steady{std::min(static_cast(Rate.pending + (Rate.rate * Timedelta * Rate.fidelity)), Rate.limit)}; + // ...and on top of it the share of the engine + return std::min(static_cast(steady + (Rate.rate * Timedelta * Rate.fidelity * engine_load(Mover))), Rate.limit); +} + +} // namespace smoke diff --git a/vehicle/smoke.h b/vehicle/smoke.h new file mode 100644 index 000000000..e9dac2622 --- /dev/null +++ b/vehicle/smoke.h @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +class TMoverParameters; + +// the smoke of a vehicle's engine, let out by the smoke sources of its model (rendering/particles.h) +namespace smoke { + +// what a smoke source spawns, before the engine of its vehicle is taken into account +struct spawn_rate { + float pending{0.f}; // particles left to spawn from the previous updates + float rate{0.f}; // particles per second + float fidelity{1.f}; // multiplier of the smoke settings + float limit{0.f}; // most particles the source can have +}; + +// number of particles the smoke source of a vehicle has to spawn after Timedelta seconds: the steady rate of the +// source, plus a share which grows with the work of its engine. a vehicle taken off the tracks (Ontracks false: gone through a portal, or waiting in the pool of the road +// traffic) puffs no more, the particles it already let out fade away as usual +float vehicle_spawn_count(TMoverParameters const &Mover, bool Ontracks, spawn_rate const &Rate, double Timedelta); + +} // namespace smoke From 088eeaa743d5e88ae39329238bc78e9da144f605 Mon Sep 17 00:00:00 2001 From: maj00r <73280030+maj00r@users.noreply.github.com> Date: Sat, 7 Nov 2026 12:32:02 +0100 Subject: [PATCH 25/32] fix(editor): a scenery saved under another name keeps one file of turntable controls, not the old one beside its own (#251) The editor writes the turntable controls on each save to _obrotnice.scm and includes it. A copy of a scenery, saved under another name, still included the file of the scenery it came from. Its save wrote a file of its own and included it too, so each turntable had its events twice. Now the save leaves out of the scenery the other files of turntable controls it includes, told by the line the editor writes at their start. Their files stay on the disk, for the scenery they were made for. A file of turntables made by hand, without that line, stays included. Co-authored-by: maj00r --- application/editormodeturntable.cpp | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp index e6b78d9ef..76b70b575 100644 --- a/application/editormodeturntable.cpp +++ b/application/editormodeturntable.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "simulation/simulation.h" #include "utilities/Globals.h" +#include "utilities/Logs.h" #include "utilities/translation.h" #include "utilities/utilities.h" #include "world/Track.h" @@ -186,6 +187,20 @@ void label(screen_projection const &Projection, glm::dvec3 const &Point, std::st drawlist->AddText(ImVec2(at.x + 12.f, at.y), IM_COL32(255, 255, 255, 235), Text.c_str()); } +// the first line of the file of the turntable controls, which tells it from the files of the scenery made by hand +char const *const kControlsHeader{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one"}; + +// true if the file is one of the turntable controls the editor writes +bool controls_file(std::string const &Path) +{ + std::ifstream input{Path, std::ios_base::binary}; + std::string first; + std::getline(input, first); + if (false == first.empty() && first.back() == '\r') + first.pop_back(); + return first == kControlsHeader; +} + } // namespace void editor_mode::scan_turntables() @@ -1143,7 +1158,7 @@ bool editor_mode::turntable_controls(std::vector &Rootstatements) auto const exists{std::filesystem::exists(path, error)}; if (tables == 0 && false == exists) return false; - auto const content{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one\r\n" + text.str()}; + auto const content{std::string{kControlsHeader} + "\r\n" + text.str()}; std::string current; { std::ifstream input{path, std::ios_base::binary}; @@ -1154,6 +1169,21 @@ bool editor_mode::turntable_controls(std::vector &Rootstatements) std::ofstream output{path, std::ios_base::binary | std::ios_base::trunc}; output << content; } + // the controls written for this scenery under another name, the file it was copied from, give way to these: with both + // included each turntable would have its events twice. their files stay on the disk, for the scenery they were made for + for (std::size_t handle = 1; handle <= scene::Layers.size(); ++handle) + { + auto const layer{static_cast(handle)}; + if (false == scene::Layers.listed(layer) || scene::Layers.layer(layer).removed) + continue; + auto name{scene::Layers.layer(layer).name}; + std::replace(name.begin(), name.end(), '\\', '/'); + auto const lower{ToLower(name)}; + if (lower == ToLower(file) || lower.ends_with("/" + ToLower(file)) || false == lower.ends_with("_obrotnice.scm")) + continue; + if (controls_file(Global.asCurrentSceneryPath + scene::Layers.layer(layer).name) && scene::Layers.remove(layer)) + WriteLog("Editor: " + name + " left out of the scenery, the turntable controls are in " + file + " now"); + } auto included{m_turntable_included}; for (std::size_t handle = 1; handle <= scene::Layers.size() && false == included; ++handle) { From ecdef7f7537bbc9d76a7938fbd8cd74f7b4da5bf Mon Sep 17 00:00:00 2001 From: Sebastian Krzyszkowiak Date: Sat, 7 Nov 2026 12:33:22 +0100 Subject: [PATCH 26/32] opengl*renderer: Show an empty frame early (#233) Gives a better feedback to the user that something has actually launched. --- rendering/opengl33renderer.cpp | 4 ++++ rendering/openglrenderer.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 76750472d..5c64988b4 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -104,6 +104,10 @@ bool opengl33_renderer::Init(GLFWwindow *Window) glDebugMessageCallback( ErrorCallback, 0 ); } + glClearColor( 0.f, 0.f, 0.f, 1.f ); + glClear( GL_COLOR_BUFFER_BIT ); + glfwSwapBuffers( Window ); + if (!Init_caps()) return false; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 7516f5a22..ef54f32c9 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -75,6 +75,10 @@ float const EU07_OPACITYDEFAULT { 0.5f }; bool opengl_renderer::Init( GLFWwindow *Window ) { + glClearColor( 0.f, 0.f, 0.f, 1.f ); + glClear( GL_COLOR_BUFFER_BIT ); + glfwSwapBuffers( Window ); + if( false == Init_caps() ) { return false; } m_window = Window; From 605a94cf2b66214ca4ebc24c841952c504a1f973 Mon Sep 17 00:00:00 2001 From: Sebastian Krzyszkowiak Date: Sat, 7 Nov 2026 12:33:46 +0100 Subject: [PATCH 27/32] uilayer: Force ui viewports to be disabled under gamescope and Steam Big Picture (#232) --- application/uilayer.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 2b1fa1686..5443d158c 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -395,10 +395,15 @@ bool ui_layer::init(GLFWwindow *Window) } // panels can be docked together, and dragged out of the simulator window into windows of their own (e.g. on another monitor) - // when both backends can do it. glfw can't place windows under wayland. both flags have to be set before the first frame + // when both backends can do it. glfw can't place windows under wayland. gamescope does not work well with popup windows. + // both flags have to be set before the first frame m_imguiio->ConfigFlags |= ImGuiConfigFlags_DockingEnable; + const char* desktop = std::getenv("XDG_CURRENT_DESKTOP"); + const char* gamepadui = std::getenv("SteamGamepadUI"); + if (!desktop) desktop = ""; + if (!gamepadui) gamepadui = ""; auto const viewportsupport{(m_imguiio->BackendFlags & ImGuiBackendFlags_PlatformHasViewports) && (m_imguiio->BackendFlags & ImGuiBackendFlags_RendererHasViewports) && - glfwGetPlatform() != GLFW_PLATFORM_WAYLAND}; + glfwGetPlatform() != GLFW_PLATFORM_WAYLAND && std::string_view{desktop} != "gamescope" && std::string_view{gamepadui} != "1"}; if (Global.ui_viewports && viewportsupport) { m_imguiio->ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; From 3a29a165a9b79aee5ee7d7f687848da0bc07b33a Mon Sep 17 00:00:00 2001 From: Sebastian Krzyszkowiak Date: Sat, 7 Nov 2026 12:34:01 +0100 Subject: [PATCH 28/32] simulationenvironment: Don't multiply fog color by diffuselevel (#229) It can easily make the fog too dark compared to the environment, producing a "fire in the forest" effect. --- simulation/simulationenvironment.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/simulation/simulationenvironment.cpp b/simulation/simulationenvironment.cpp index 14ad2353c..bdc4b0598 100644 --- a/simulation/simulationenvironment.cpp +++ b/simulation/simulationenvironment.cpp @@ -167,9 +167,8 @@ world_environment::update() { // the impact of sun component is reduced proportionally to overcast level, as overcast increases role of ambient light auto const diffuselevel = std::lerp( keylightintensity, intensity * ( 1.0f - twilightfactor ), 1.0f - std::min( 1.f, Global.Overcast ) * 0.75f ); // ...update light colours and intensity. - keylightcolor = keylightcolor * diffuselevel; - Global.DayLight.diffuse = glm::vec4( keylightcolor, Global.DayLight.diffuse.a ); - Global.DayLight.specular = glm::vec4( keylightcolor * 0.85f, diffuselevel ); + Global.DayLight.diffuse = glm::vec4( keylightcolor * diffuselevel, Global.DayLight.diffuse.a ); + Global.DayLight.specular = glm::vec4( keylightcolor * diffuselevel * 0.85f, diffuselevel ); // tonal impact of skydome color is inversely proportional to how high the sun is above the horizon // (this is pure conjecture, aimed more to 'look right' than be accurate) From 55ba7a9f41e463dcb94f2adaf6859d8563df316c Mon Sep 17 00:00:00 2001 From: pawelw Date: Sat, 7 Nov 2026 12:35:55 +0100 Subject: [PATCH 29/32] Instancing: skip the buckets of a cell lying wholly beyond the range of their instances (#225) * perf(instancing): skip a cell's buckets lying wholly beyond the range of their instances * perf(instancing): go only through the instances drawn one by one in the per-node pass * perf(instancing): leave out of the color pass the cells holding only instances too far to be drawn * refactor(instancing): keep the caches of the instances of a cell without mutable fields, set by the renderer when the cell is queued --- model/AnimModel.cpp | 3 ++ model/AnimModel.h | 3 ++ rendering/opengl33renderer.cpp | 93 +++++++++++++++++++++++++++++++++- rendering/opengl33renderer.h | 6 +++ scene/scene.cpp | 3 ++ scene/scene.h | 13 +++++ 6 files changed, 119 insertions(+), 2 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 770790c1e..cafb3a93e 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -514,6 +514,7 @@ bool submodel_tree_blocks_instancing( TSubModel const *Sub ) { } // anonymous namespace int TAnimModel::s_instanceable_total = 0; +unsigned int TAnimModel::s_instanceable_revoked = 0; int TAnimModel::s_classified_total = 0; int TAnimModel::s_rejected_no_pmodel = 0; int TAnimModel::s_rejected_lights = 0; @@ -551,6 +552,7 @@ std::shared_ptr TAnimModel::AddContainer(std::string const &Name m_animlist.push_back(tmp); // containers are added by events after the instance was loaded and bucketed; // the instanced path skips RaAnimate(), so this instance has to be drawn on its own + if( m_instanceable ) { ++s_instanceable_revoked; } m_instanceable = false; return tmp; } @@ -780,6 +782,7 @@ void TAnimModel::SkinSet( int const Index, material_handle const Material ) { m_materialdata.replacable_skins[ std::clamp( Index, 1, 4 ) ] = Material; // the instance stays in the bucket of its old skin set, where it would get the textures of the batch + if( m_instanceable ) { ++s_instanceable_revoked; } m_instanceable = false; } diff --git a/model/AnimModel.h b/model/AnimModel.h index 0087418df..2cf1003bb 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -237,6 +237,9 @@ public: // diagnostic counters (process-wide). Updated inside update_instanceable_flag() // so the renderer can surface load-time classification stats in the debug overlay. static int s_instanceable_total; // count of TAnimModel*'s classified instanceable + // bumped whenever an instance loses its instanceable status after it was put in a scene cell (an event adds + // an animation container, or changes a skin); the cells rebuild their lists of instances drawn one by one then + static unsigned int s_instanceable_revoked; static int s_classified_total; // count of TAnimModel*'s that ran through the classifier static int s_rejected_no_pmodel; static int s_rejected_lights; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 5c64988b4..33346fad2 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2666,6 +2666,12 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc { if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) { + // a cell with nothing but instances too far to be drawn is left out of the color pass altogether + if( ( true == Prepare_Instances( cell ) ) + && ( m_renderpass.draw_mode == rendermode::color ) + && ( true == Cell_Out_Of_Range( cell ) ) ) { + continue; + } // store visible cells with content as well as their current distance, for sorting later m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); } @@ -2680,6 +2686,7 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc { if ((true == cell.m_active) && (m_renderpass.pass_camera.visible(cell.m_area))) { + Prepare_Instances( cell ); // store visible cells with content as well as their current distance, for sorting later m_cellqueue.emplace_back(glm::length2(m_renderpass.pass_camera.position() - cell.m_area.center), &cell); } @@ -2871,8 +2878,10 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite Queue_Instances( cell ); } } - // remaining (non-instanceable) opaque instance nodes go through the per-node path - for (auto *instance : cell->m_instancesopaque) + // remaining (non-instanceable) opaque instance nodes go through the per-node path. the list of them kept for + // the cell spares going through every instance of it only to skip the instanceable ones - with many of these + // (vegetation) that took more time than drawing everything else + for (auto *instance : ( Instance_Cache_Valid( *cell ) ? cell->m_instancecache.single : cell->m_instancesopaque )) { if( instance->m_instanceable ) { continue; } // already handled via bucket Render(instance); @@ -3178,6 +3187,11 @@ void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) auto const distancelimit { slack * ( reflections ? sq( static_cast( Global.reflectiontune.range_instances ) ) : sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) }; // up close the rounding of the copies is too large a part of the distance for the slack to cover it auto const distancetested { sq( 64.0 ) }; + // the nearest point of the bounding sphere of the cell, which holds the locations of all its instances. no instance + // of the cell is closer, so where this distance fails the range test below for the largest range in a bucket, + // it fails it for each instance of that bucket, and the bucket can be skipped without going through them + auto const cellnearestdistance { std::max( 0.0, glm::length( Cell->m_area.center - cameraposition ) - Cell->m_area.radius ) }; + auto const cellnearestdistancesquared { cellnearestdistance * cellnearestdistance }; for( auto const &bucket : Cell->m_instancebuckets_opaque ) { auto const &instances { bucket.second.instances }; @@ -3190,6 +3204,11 @@ void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) || ( bucket.second.bounds.size() != instances.size() ) ) { Update_Instance_Bounds( bucket.first.pModel, bucket.second ); } + else if( ( bucket.second.rangeversion == m_instanceboundsversion ) + && ( cellnearestdistancesquared > distancetested ) + && ( cellnearestdistancesquared * distancescale + distanceoffset >= slack * bucket.second.rangesquaredmax ) ) { + continue; + } auto const &bounds { bucket.second.bounds }; std::vector *dest { nullptr }; // looked up when there's something to put in it for( std::size_t idx = 0; idx < bounds.size(); ++idx ) { @@ -3210,6 +3229,76 @@ void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) } } +// brings up to date what the renderer keeps of the instances of the cell: the copies of the bounds of the instances +// and the largest range in each bucket, the largest range in the cell, and the list of its instances drawn one by one. +// rebuilt after the content of the cell changed, or any instance lost its instanceable status (an event added an +// animation container, or changed a skin). returns: true if the cache can be used; it can't in the editor, which +// moves and resizes the instances in place, as with the copies of their bounds +bool opengl33_renderer::Prepare_Instances( scene::basic_cell &Cell ) +{ + if( EditorModeFlag ) { return false; } + if( Instance_Cache_Valid( Cell ) ) { return true; } + + auto &cache { Cell.m_instancecache }; + cache.rangesquaredmax = 0.f; + for( auto &[ key, bucket ] : Cell.m_instancebuckets_opaque ) { + if( ( bucket.boundsversion != m_instanceboundsversion ) + || ( bucket.bounds.size() != bucket.instances.size() ) ) { + Update_Instance_Bounds( key.pModel, bucket ); + } + auto rangesquaredmax { 0.f }; + for( auto const &instancebounds : bucket.bounds ) { + rangesquaredmax = std::max( rangesquaredmax, instancebounds.rangesquaredmax ); + } + bucket.rangesquaredmax = rangesquaredmax; + bucket.rangeversion = m_instanceboundsversion; + cache.rangesquaredmax = std::max( cache.rangesquaredmax, rangesquaredmax ); + } + cache.single.clear(); + for( auto *instance : Cell.m_instancesopaque ) { + if( false == instance->m_instanceable ) { + cache.single.emplace_back( instance ); + } + } + cache.instancedonly = + ( false == Cell.m_instancebuckets_opaque.empty() ) + && Cell.m_paths.empty() + && Cell.m_shapesopaque.empty() + && Cell.m_shapestranslucent.empty() + && Cell.m_lines.empty() + && Cell.m_traction.empty() + && Cell.m_instancetranslucent.empty() + && cache.single.empty(); + cache.boundsversion = m_instanceboundsversion; + cache.revoked = TAnimModel::s_instanceable_revoked; + return true; +} + +bool opengl33_renderer::Instance_Cache_Valid( scene::basic_cell const &Cell ) const +{ + return ( false == EditorModeFlag ) + && ( Cell.m_instancecache.boundsversion == m_instanceboundsversion ) + && ( Cell.m_instancecache.revoked == TAnimModel::s_instanceable_revoked ); +} + +// true if nothing of the cell would be drawn by the color pass: it holds only instances drawn with the instanced path, +// and the nearest point of the cell is past the largest range of them. the test is the one made for each bucket in +// Queue_Instances(), done before the cell is put in the cell queue: a scenery with many small instances of a short +// range has them in nearly every cell up to the draw range, and queueing, sorting and visiting these cells only +// to skip all their buckets took a good part of the frame +bool opengl33_renderer::Cell_Out_Of_Range( scene::basic_cell const &Cell ) const +{ + if( ( false == Instance_Cache_Valid( Cell ) ) || ( false == Cell.m_instancecache.instancedonly ) ) { return false; } + // same measures as in Queue_Instances() for the color pass + auto const distancescale { 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) }; + auto const slack { 1.001 }; + auto const distancetested { sq( 64.0 ) }; + auto const cellnearestdistance { std::max( 0.0, glm::length( Cell.m_area.center - m_renderpass.pass_camera.position() ) - Cell.m_area.radius ) }; + auto const cellnearestdistancesquared { cellnearestdistance * cellnearestdistance }; + return ( cellnearestdistancesquared > distancetested ) + && ( cellnearestdistancesquared * distancescale >= slack * Cell.m_instancecache.rangesquaredmax ); +} + // makes the copies of the bounds of the instances held by a bucket of a scene cell void opengl33_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ) { diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index d48055f9f..b472849bc 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -302,6 +302,12 @@ class opengl33_renderer : public gfx_renderer { // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), // less the ones the copies of their bounds show to be out of sight in the current render pass void Queue_Instances( scene::basic_cell const *Cell ); + // brings up to date what the renderer keeps of the instances of the cell; returns: true if the cache can be used + bool Prepare_Instances( scene::basic_cell &Cell ); + // true if the cache of the cell is up to date + bool Instance_Cache_Valid( scene::basic_cell const &Cell ) const; + // true if nothing of the cell would be drawn by the color pass from the camera of the pass + bool Cell_Out_Of_Range( scene::basic_cell const &Cell ) const; // makes the copies of the bounds of the instances held by a bucket of a scene cell void Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ); bool Render(TDynamicObject *Dynamic); diff --git a/scene/scene.cpp b/scene/scene.cpp index 7b0e29940..312765c4d 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -352,6 +352,7 @@ void basic_cell::insert( TAnimModel *Instance ) { m_active = true; + m_instancecache.boundsversion = 0; // the renderer builds it anew auto const flags = Instance->Flags(); auto alpha = @@ -454,6 +455,8 @@ basic_cell::unregister_end( TTrack *Path ) { void basic_cell::erase( TAnimModel *Instance ) { + m_instancecache.boundsversion = 0; // the renderer builds it anew + auto const flags = Instance->Flags(); auto alpha = Instance->Material() != nullptr ? Instance->Material()->textures_alpha : 0x30300030; diff --git a/scene/scene.h b/scene/scene.h index 086e2e830..0f52fcdbd 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -252,6 +252,18 @@ public: // caches owned by the renderer; emptied whenever the content of the bucket changes mutable std::vector bounds; // copies for the instances, in the same order mutable unsigned int boundsversion { 0 }; + // the largest rangesquaredmax of the copies - past it no instance of the bucket is drawn - and the version + // of the bounds it was taken with. set by the renderer before the cell is drawn, see Prepare_Instances() + float rangesquaredmax { 0.f }; + unsigned int rangeversion { 0 }; + }; + // what the renderer keeps of the instances of the cell, set by it before the cell is drawn + struct instance_cache { + instance_sequence single; // the opaque instances drawn one by one, as they aren't instanceable + float rangesquaredmax { 0.f }; // the largest rangesquaredmax of the instance buckets + bool instancedonly { false }; // the cell holds nothing to draw but instances drawn with the instanced path + unsigned int boundsversion { 0 }; // version of the bounds of the renderer it was built with, 0: not built + unsigned int revoked { 0 }; // TAnimModel::s_instanceable_revoked at the time of the build }; using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using sound_sequence = std::vector; @@ -275,6 +287,7 @@ public: // m_instancesopaque for nodes whose TAnimModel::m_instanceable == true. // The renderer uses these to amortise per-model state setup across many instances. instance_bucketdata_map m_instancebuckets_opaque; + instance_cache m_instancecache; traction_sequence m_traction; sound_sequence m_sounds; eventlauncher_sequence m_eventlaunchers; From bb009b049993c84cc4f84c4d7f7c3bf0c42ea742 Mon Sep 17 00:00:00 2001 From: pawelw Date: Sat, 7 Nov 2026 12:36:17 +0100 Subject: [PATCH 30/32] Text displays: draw opaque characters in the opaque pass (#201) * Text displays: accept characters above 127, in windows-1250 or utf-8 A text display (sub-model of type "text") looks up the sub-model of each character in a 256 entry table, and indexed it with a plain char. With char being signed, any character above 127 gave a negative index: reading the text went outside the table, and so did filling the table if a character sub-model had such a name. In effect the displays could only show ASCII, and a text with national characters could crash. The lookup is now keyed by unicode code point. Both the displayed text and the names of the character sub-models are decoded as utf-8 if they are valid utf-8, and as windows-1250 otherwise, so legacy (ansi) sceneries and models work next to utf-8 ones, and the two can be mixed. A short ansi text can be valid utf-8 by accident (two upper case accented letters in a row will do); when both readings are possible the one which finds more of its characters in the model is used. The rest is unchanged: the first character of the text still isn't displayed, the characters are still drawn in the translucent pass only. A missing text pointer no longer gets dereferenced. * Text displays: draw opaque characters in the opaque pass The characters of a text display were only laid out in the translucent pass, so a display had to be made of translucent sub-models to show anything. Those don't write depth: overhead wires and other translucent geometry drawn later showed through the display, and translucent geometry in front of it could end up painted over. The opaque pass now lays the characters out the same way, so a display built from opaque sub-models is drawn with depth like any other opaque geometry (and casts shadows). Translucent characters keep working as before. A model with a text display shows the text of its own instance, so such models are excluded from the GPU-instanced opaque path. * Text displays: prepare the characters once for each text The sequence of character sub-models was cached for the last text only, so displays of several instances drawn in turn decoded their texts again on every draw. It is now kept for each text the display shows; the texts of the instances don't change, so a draw costs a single lookup. Also the comments added in the renderers are now in English. * Text displays: move the text decoding to utilities utf8_to_utf32() and win1250_to_utf32(), with the windows-1250 table, now sit next to win1250_to_utf8(). The existing function maps only the Polish letters and passes other bytes above 127 through, so it can't serve the displays. * Text displays: address the SonarCloud findings std::array instead of the C-style arrays, the utf-8 sequence length worked out in a function of its own instead of nested conditional operators, std::byte for the bit operations, std::ranges::count_if and contains(). The character loop of the renderers moved to Render_Alpha_text(), which takes the nesting back to the limit, and the static pasText is accessed through the class. * Text displays: move the opaque character loop to Render_text() The same as for the translucent pass: the loop sits in a function of its own and the static pasText is accessed through the class, which keeps the nesting within the limit. * Text displays: access the static iAlpha through the class in the opaque pass * Text displays: transparent lookup of the text layouts, init-statement for the lookup result Two SonarCloud findings left in the code of #198: the map of the text layouts gets a transparent hasher with std::equal_to<>, and the result of the lookup is declared in the if statement. --- model/AnimModel.cpp | 2 ++ model/Model3d.cpp | 3 +-- model/Model3d.h | 9 ++++++++- rendering/opengl33renderer.cpp | 29 +++++++++++++++++++++++++++-- rendering/opengl33renderer.h | 1 + rendering/openglrenderer.cpp | 25 +++++++++++++++++++++++-- rendering/openglrenderer.h | 3 +++ 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index cafb3a93e..52a879ac2 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -505,6 +505,8 @@ bool anim_type_shared_by_instances( TAnimType a ) { // instanced path (one shared submodel tree across all instances) cannot serve. bool submodel_tree_blocks_instancing( TSubModel const *Sub ) { if( Sub == nullptr ) { return false; } + // a text display shows the text of its instance, a tree shared by all instances can't do that + if( Sub->eType == TP_TEXT ) { return true; } if( anim_type_unsafe_for_instancing( Sub->b_Anim ) ) { return true; } if( ( ( Sub->iFlags & 0x4000 ) != 0 ) && ( false == anim_type_shared_by_instances( Sub->b_Anim ) ) ) { return true; } if( submodel_tree_blocks_instancing( Sub->Child ) ) { return true; } diff --git a/model/Model3d.cpp b/model/Model3d.cpp index 66fc08c94..44f5f6255 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -898,8 +898,7 @@ std::vector const &TSubModel::text_letters(std::string const &Text) { // the sequence is prepared once for each text the display shows. the texts of the model instances don't change, // so drawing a display afterwards costs a single lookup, however many instances and passes take turns - auto const prepared{m_textlayouts.find(Text)}; - if (prepared != m_textlayouts.end()) + if (auto const prepared{m_textlayouts.find(Text)}; prepared != m_textlayouts.end()) { return prepared->second; } diff --git a/model/Model3d.h b/model/Model3d.h index 27565c206..a9ef8af12 100644 --- a/model/Model3d.h +++ b/model/Model3d.h @@ -15,6 +15,8 @@ http://mozilla.org/MPL/2.0/. #include "model/material.h" #include "gl/query.h" +#include + #define EU07_USE_GEOMETRYINDEXING // Ra: specjalne typy submodeli, poza tym GL_TRIANGLES itp. @@ -159,7 +161,12 @@ public: // chwilowo // text display (TP_TEXT) data, filled on first use std::unordered_map m_letters; // sub-models of the available characters, by unicode code point bool m_lettersready { false }; // whether the character lookup was built yet - std::unordered_map> m_textlayouts; // sub-models of consecutive characters, for each text shown + // transparent hasher, so the text layouts can be looked up with any string type without a temporary std::string + struct text_hash { + using is_transparent = void; + std::size_t operator()( std::string_view const Text ) const noexcept { return std::hash{}( Text ); } + }; + std::unordered_map, text_hash, std::equal_to<>> m_textlayouts; // sub-models of consecutive characters, for each text shown TSubModel *Parent{ nullptr }; // nadrzędny, np. do wymnażania macierzy int iVisible { 1 }; // roboczy stan widoczności float fVisible { 1.f }; // visibility level diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 33346fad2..c24f750c8 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -4152,6 +4152,23 @@ bool opengl33_renderer::Render(TModel3d *Model, material_data const *Material, f return result; } +// opaque characters of a text display, laid out in a row the same way as the translucent ones +void opengl33_renderer::Render_text(TSubModel *Submodel) +{ + if (TSubModel::pasText == nullptr) + { + return; + } + ::glPushMatrix(); + for (auto *p : Submodel->text_letters(*TSubModel::pasText)) + { + Render(p); + if (p->fMatrix) + ::glMultMatrixf(p->fMatrix->readArray()); // move on to the place of the next character + } + ::glPopMatrix(); +} + void opengl33_renderer::Render(TSubModel *Submodel) { glDebug("Render TSubModel"); @@ -4302,9 +4319,17 @@ void opengl33_renderer::Render(TSubModel *Submodel) } } } - if (Submodel->Child != nullptr) - if (Submodel->iAlpha & Submodel->iFlags & 0x001F0000) + if (Submodel->Child != nullptr && (TSubModel::iAlpha & Submodel->iFlags & 0x001F0000)) + { + if (Submodel->eType == TP_TEXT) + { + Render_text(Submodel); + } + else + { Render(Submodel->Child); + } + } if (Submodel->iFlags & 0xC000) { diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index b472849bc..1b4897386 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -314,6 +314,7 @@ class opengl33_renderer : public gfx_renderer { bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance); void Render(TSubModel *Submodel); + void Render_text(TSubModel *Submodel); // opaque characters of a text display void Render(TTrack *Track); void Render(scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last); // renders the per-track sleeper instances (TTrack::m_sleeper_local_transforms) via GPU instancing. diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index ef54f32c9..180dba771 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -2794,6 +2794,21 @@ opengl_renderer::Render( TModel3d *Model, material_data const *Material, float c return result; } +// opaque characters of a text display, laid out in a row the same way as the translucent ones +void +opengl_renderer::Render_text( TSubModel *Submodel ) { + + if( TSubModel::pasText == nullptr ) { return; } + + ::glPushMatrix(); + for( auto *p : Submodel->text_letters( *TSubModel::pasText ) ) { + Render( p ); + if( p->fMatrix ) + ::glMultMatrixf( p->fMatrix->readArray() ); // move on to the place of the next character + } + ::glPopMatrix(); +} + void opengl_renderer::Render( TSubModel *Submodel ) { @@ -3112,9 +3127,15 @@ opengl_renderer::Render( TSubModel *Submodel ) { } } } - if( Submodel->Child != nullptr ) - if( Submodel->iAlpha & Submodel->iFlags & 0x001F0000 ) + if( Submodel->Child != nullptr + && ( TSubModel::iAlpha & Submodel->iFlags & 0x001F0000 ) ) { + if( Submodel->eType == TP_TEXT ) { + Render_text( Submodel ); + } + else { Render( Submodel->Child ); + } + } if( Submodel->iFlags & 0xC000 ) ::glPopMatrix(); diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 9779388c7..3a3538a6c 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -237,6 +237,9 @@ private: Render( TModel3d *Model, material_data const *Material, float const Squaredistance ); void Render( TSubModel *Submodel ); + // opaque characters of a text display + void + Render_text( TSubModel *Submodel ); void Render( TTrack *Track ); void From 466cbbbfa157525a51738f9d9715225ca066d7ce Mon Sep 17 00:00:00 2001 From: jerrrrycho Date: Sat, 7 Nov 2026 12:37:12 +0100 Subject: [PATCH 31/32] Test/core unit coverage (#235) * test: real vehicles and golden traces in the unit tests The game's own vehicles (dynamic/ of the release used for the tests) as JSON of what the game's vehicle loader reads of their .fiz files, includes expanded: test_fiz.cpp converts them on demand and checks the loader reads the same sections, values and rows as of the game's file (fiz.h). Chosen to cover every engine type, brake valve, brake handle, local brake, coupler type, door type and brake delay set the game data uses. Golden traces (golden.h) characterise the simulation, compared within a relative tolerance or exactly, re-recorded with EU07_UNIT_TESTS_UPDATE_GOLDEN=1: - the parameters of every real vehicle as loaded, - the brakes of every real vehicle through charge, service steps, release, overcharge and emergency, on every brake delay setting, - every position of every driver's and local brake handle, - every powered vehicle started up the way the AI does it, driven to full power and braked to a stop (drive.h), - the real electric units and a locomotive with wagons driven whole, as trains of coupled vehicles stepped the way the game steps them (consist.h), - scenarios of the controls (scenario.h): doors, reversing and cab change, slip and sand, emergency stops, releasers, the ep and the electrodynamic brake, heating, loading, a collision, and frames of several physics iterations. * test: commands passed between vehicles Every command RunCommand knows run on every real vehicle and recorded, commands travelling through a train with a vehicle facing backwards, a coupling without the control cable, and loading and unloading by commands. * test: every switch of the cab, sent to the next vehicle Each switch sending its state to the train, each way and to each range, with the controllers and direction among them, on every real vehicle driving another of its kind, recorded for both. * test: the switches of the cab worked on a pair under the catenary The same switches with two seconds for the vehicles to act on each, so the pantographs rise, the main switch closes and the auxiliaries start. * test: gauges of trains in multiple and the damage descriptions The ammeters and the brake force the AI expects, recorded on the electric units and locomotives in multiple driven, the damage described on the driver's panel and ammeters showing the current of controlled vehicles. * test: give load commands their place, as the game does The loading test set its commands with SetInternalCommand, which leaves TCommand::Location as it is: uninitialised in a fresh vehicle, so the distance check of Load= and UnLoad= read garbage and the test failed now and then. The game gives them through PutCommand with the place of the platform, and so do the tests now. * test: brake valves and handles no vehicle of the game data has Fitted to a wagon and to an EU07: the valve variants and handles the loader knows that no real vehicle uses, the CV1 ones and the test handle among them, and a name the loader doesn't know. * test: braking from speed with the automatic ED brake, and a draisine Trains with the EP09, the E6ACT and the EW58 braked by ep, light and full service applications from speed, and the draisine pumped from rest and rolling. Only the E6ACT brings its electrodynamic brake in here. * test: the EP09's electrodynamic brake in service braking held to a stop start_up() took a vehicle for ready once its line breaker was closed and its reservoir full, which a train made already moving is at once: the EP09's manually started converter was never switched on, so there was no 110 V and no electrodynamic brake. It now waits for the converter. The EP09 with wagons is braked from 120 km/h by light, medium, high and full service applications held to a stop: the motors brake, the air of its cylinders is let out and given back as the train slows down. The flag of the electrodynamic brake stays on at a standstill below 2.1 bar. C strings given to CAPTURE are shown as text. * test: the converter switched on before a train is ready, the EW58's ep-staged electrodynamic brake start_up() called a train ready as soon as its line breaker closed and the air was there, so the converter of a unit having it in another car than the cab (the EW58's middle car) was never switched on, and its electrodynamic brake, gated by the 110 V, never came in. The converter and the compressor now always get switched on first. The EW58 brakes electrically by the stages of the ep brake, not by the pipe alone. * test: the ET42 and the 214M railbus among the real vehicles The ET42's halves (its own series motor branch: the motors in series at the high current setting) and the 214MA, a diesel railbus with the hydraulic torque converter, the retarder and the controller of an induction motor drive, converted from the game data and recorded in every sweep of the real vehicles. * test: the wagons' hand brakes released with the train's brakes; the 754, SM31 and SU46 release_brakes() released the hand brake of the driver's vehicle only, so every wagon set up braked kept its hand brake on and got dragged along. It now releases the hand and spring brakes of the whole train, as TController::AutoRewident() does when the AI takes it over, and the traces of the trains with wagons are recorded anew. The 754, the SM31 and the SU46 join the real vehicles for the kinds of the diesel-electrics' field shunting relays the others lack (RelayType 0, 1 and 46). * test: traction on a lost and too high catenary voltage, the ET42, the diesel-electrics' shunting relays The line breaker opened by the voltage relays of a series motor locomotive and by the limits of an induction motor one. The ET42 at the high current setting, which keeps its halves' motors in series, and its switched electrodynamic brake, whose current trips the overload relay from the top speed of the low setting. The field shunting of the diesel-electrics through each kind of their relays, at full power and notched back. * test: the EW58's electrodynamic brake stages, the EU47's speed control and braking at the voltage's limit The stages of the EW58's motor brake set by the positions of its ep handle, the first one alone over 100 km/h. The EU47's second controller holding, raising and lowering the speed set, its local brake setting the flag of the electrodynamic brake without the motors braking, and its brake feeding back less current into a catenary over the drive's MaxV. * test: diesel engines too cold and too hot to start, the 214M railbus's controller and retarder The heat of the diesel engines: at -10 degrees the ones with the lowest water temperature set don't start until the heater warms them, at 88 degrees the ones with a second water circuit don't start while it cools, and the SM42 run hot at full power is kept under its highest temperature by its fans and shutters. The 214M railbus's controller from the air brake through the retarder and neutral to power, its torque converter's lock-up and its gearbox. The controller helpers of the traction tests (full power, waiting for the line contactors, and notching back) move to test::scenario. * test: couplers coupled, uncoupled, broken and colliding; loads assigned, loaded and unloaded Coupling by hand touching or apart and of the same kind or not, with the sounds of what changes; uncoupling a screw coupler pressed together, stretched, with the hoses alone or broken; the automatic couplers of electric units coupling on contact, without the control cable between two kinds; a coupler broken with its brake hose; collisions over 15 km/h derailing. The pantographs set up by the pantstate load, a load of one kind at a time up to the capacity, and the steps of loading and unloading. * test: the battery drained and charged, the pantographs' tank and pressure switch, the 214M's speed control The EU07's battery drained without the converter by the lights and the small compressor until it opens the line breaker and nothing starts, charged by the converter, and switched off. The pantographs on their own leaking tank until the pressure switch opens the line breaker, and the small compressor pumping it up and priming the switch. The 214M railbus's speed control set by its buttons, steps and power limit, braking down with the retarder, and standing by. * test: the MHZ handles' buttons, the valves at speed, every brake material, the NESt3's relays The overload and the high pressure buttons of the MHZ handles. The KE and the LSt at speed in the R and P settings, under the anti-slip request and the releaser button. Every material of the brake shoes and disks the loader takes, braking a wagon from speed, the ones no game vehicle has among them. The CV1_R and the Oerlikon valve's relays set by its name (-ED, AL2 without ESt3) fitted to a wagon. * test: the loader's lists and lines: the 218M, SA134 and 22WEc, a compressor list, Pmax by speed, skipped lines The 218M (V2NList), the SA134 (HTCList) and the Elf 22WEc (ffBrakeList) join the real vehicles for the lists the others lack. Definitions made by the tests for what no game file has: a compressor list, whose table of whole numbers reads a factor of 0.9 as 0; the compressor's limits apart for the rear cab; the highest power of an induction motor drive by the speed, extrapolated past the table; and the lines the loader skips or trims (comments, a hash anywhere, leading spaces, carriage returns). * test: what the cab and the AI call on a vehicle: gauges of the train, the split ED brake, spring brake, cut-off The engine rotation of up to three vehicles in multiple and the lamps of the overload relay and the starting resistors read through the control cable; the engine's idle speed and the ET42's second controller shown in its dynamic brake; the controller of the electrodynamic brake apart from the local brake, which no game vehicle has yet; the spring brake shut off and released, the ep brake's fuse and the handle stepped by the AI; damaged motors cut off; the cab activated by itself; a road vehicle moved across a road, for which the tests' track gets a width; and the lamps and the buzzer of the security system left unattended. * test: the EP05 and its camshaft among the real vehicles, the headlights' colour and brightness The EP05, whose camshaft closes the line breaker and the line contactors by the camshaft's own position, converted from the game data and recorded in every sweep of the real vehicles. The Headlights section, which only the 214M's own file has, in a definition made by the test. * test: what the unit tests reach of the simulation core, what's dead or left, and what they found COVERAGE.md: the blocks of Mover.cpp, hamulce.cpp, Oerlikon_ESt.cpp and friction.cpp the tests run, the code nothing in the game runs, the code only data no game vehicle has would run, what's reachable and left, and the behaviour the tests found that may deserve a look. * test: check invariants of the physics on every step of every golden trace Rules any vehicle keeps whatever a test does (no NaN, cylinders and pipes within their limits, no current with the line breaker or contactors open, no force without power, distance as the speed runs, no acceleration over 5 g), checked on every step, with no comparison to a recorded file. The traces where a test or a game vehicle breaks one are excused with the reason; the vehicles' cases are in the findings of COVERAGE.md. * test: key figures of every golden trace, per part of the run Duration, speeds, distance, time to stop, the brake pipe's lowest pressure and time to recover, the peaks of the cylinders, the motor current and the tractive force, for each part between two events, in data/golden/.figures. Compared within 0.5% or the last decimal, so they hold on any compiler, and an intended change of behaviour shows in a few readable lines. Traces sampled by hand keep none. * test: keep the golden samples out of the repository, recorded on the code before a change The samples of the golden traces are 29 MB and only mean something against the code before the change they check, so they're recorded locally before it (EU07_UNIT_TESTS_UPDATE_GOLDEN=1) and compared after it, and ignored by git. The key figures and the invariants stay, and are what the CI checks. Without samples recorded a trace compares only its key figures, but with EU07_UNIT_TESTS_GOLDEN_EXACT=1, the check of a change which shouldn't alter anything, it fails, so the check can't pass by having nothing to compare. * fix: draw the noise of the control pipe pressure in one order on every compiler The order of the two Random(10) calls in 'Random(10) - Random(10)' is unspecified: GCC draws them the other way round than MSVC Release, which mirrored the control pipe pressure around 5 bar and failed 32 unit tests on the Linux CI. The draws are sequenced in the order of MSVC Release, which the golden traces were recorded with. * test: count the start-up rounds with an integer, not a double (SonarCloud S2193) * test: behaviour in CHECKs, the key figures out of the repository The key figures of the golden traces are recorded with their samples on the code before a change, and compared after it, instead of being kept: a trace without them checks the invariants and its test's CHECKs, which is what the CI runs. The scenarios of the controls state what they show in CHECKs, and the runs of every real vehicle what holds for each of them. Writing them showed scenarios which didn't do what they were named for: the cab change stopped in the engine room, the slip runs put the controller to full power at once and never got the line contactors closed, and the train stopped by the alarm chain never got its pipe charged again. They do it now; the rest went to the findings. * test: the SonarCloud findings in the unit tests std::format for the texts put together, emplace_back, const references, string_view parameters, std::array and std::vector for the C arrays, the ranges algorithms, names no longer shadowing the coupler ends, comments in the empty loops; start_up(), trace::check() and the converter of the definitions split up. BrakeForceR() is const, which it always was in effect, so the gauges test needs no const_cast. The golden traces compare bit for bit. --- McZapkie/MOVER.h | 2 +- McZapkie/Mover.cpp | 7 +- tests/unit/CMakeLists.txt | 14 +- tests/unit/COVERAGE.md | 117 ++++ tests/unit/README.md | 118 +++- tests/unit/consist.cpp | 106 +++ tests/unit/consist.h | 57 ++ tests/unit/data/fiz/11xa_v2/93a.json | 14 + tests/unit/data/fiz/14xa_v1/173a.json | 14 + tests/unit/data/fiz/168a_v1/168a.json | 14 + tests/unit/data/fiz/21xm_v1/214ma.json | 78 +++ tests/unit/data/fiz/21xm_v1/218ma_a_kd.json | 84 +++ tests/unit/data/fiz/3wl_v2/3wl.json | 11 + tests/unit/data/fiz/430w_v2/430w.json | 11 + tests/unit/data/fiz/4e_v1/4e.json | 81 +++ tests/unit/data/fiz/README.md | 66 ++ tests/unit/data/fiz/bdhpumn_v3/bdhpumn.json | 14 + tests/unit/data/fiz/bhp_v1/bhp-b.json | 15 + tests/unit/data/fiz/br285_v1/br285.json | 69 ++ tests/unit/data/fiz/cd/754_v1/754.json | 56 ++ tests/unit/data/fiz/cd/810_v2/810.json | 41 ++ tests/unit/data/fiz/dr2_v1/dr2.json | 13 + tests/unit/data/fiz/e186_v2/eu47.json | 61 ++ tests/unit/data/fiz/e6act_v2/e6acta.json | 41 ++ tests/unit/data/fiz/ed72_v1/5bs-ra.json | 25 + tests/unit/data/fiz/ed72_v1/5bs-rb.json | 25 + tests/unit/data/fiz/ed72_v1/6bs-sa.json | 47 ++ tests/unit/data/fiz/ed72_v1/6bs-sb.json | 47 ++ tests/unit/data/fiz/elf_v1/21we-a.json | 52 ++ tests/unit/data/fiz/elf_v1/21we-b.json | 25 + tests/unit/data/fiz/elf_v1/21we-c.json | 52 ++ tests/unit/data/fiz/elf_v1/22wec-a.json | 54 ++ .../data/fiz/en57_v1/5bgaiii_led_spot.json | 26 + tests/unit/data/fiz/en57_v1/5bgb.json | 26 + tests/unit/data/fiz/en57_v1/6ba_840.json | 26 + tests/unit/data/fiz/en57_v1/6bai.json | 25 + tests/unit/data/fiz/en57akl_v1/akl_ra.json | 40 ++ tests/unit/data/fiz/ep05_v2/ep05.json | 84 +++ tests/unit/data/fiz/ep09_v2/104e.json | 76 +++ tests/unit/data/fiz/et22_v3/201e-rn.json | 86 +++ tests/unit/data/fiz/et22_v3/201e-w.json | 86 +++ tests/unit/data/fiz/et42_v2/112e-a.json | 77 +++ tests/unit/data/fiz/et42_v2/112e-b.json | 77 +++ tests/unit/data/fiz/eu04_v1/eu04-01.json | 67 ++ tests/unit/data/fiz/ew58_v1/ew58-d.json | 20 + tests/unit/data/fiz/ew58_v1/ew58-sa.json | 68 ++ tests/unit/data/fiz/ew58_v1/ew58-sb.json | 68 ++ tests/unit/data/fiz/impuls_v1/31we-a_pr.json | 50 ++ tests/unit/data/fiz/impuls_v1/31we-b_pr.json | 20 + tests/unit/data/fiz/impuls_v1/31we-c_pr.json | 20 + tests/unit/data/fiz/impuls_v1/31we-d_pr.json | 50 ++ tests/unit/data/fiz/road/man/nl293.json | 36 + tests/unit/data/fiz/sa134_v1/218mc_a.json | 94 +++ tests/unit/data/fiz/sieciowiec_v1/a.json | 13 + tests/unit/data/fiz/sm03_v1/sm03.json | 41 ++ tests/unit/data/fiz/sm31_v1/411d.json | 43 ++ tests/unit/data/fiz/sm40_v1/sm40.json | 48 ++ tests/unit/data/fiz/sm42_v1/6d1.json | 42 ++ tests/unit/data/fiz/sn61_v2/sn61.json | 42 ++ tests/unit/data/fiz/st44_v2/m62-1151.json | 56 ++ tests/unit/data/fiz/st45_v2/301dd.json | 55 ++ .../unit/data/fiz/static/sug/sug_kielce.json | 70 ++ tests/unit/data/fiz/su45_v2/301d.json | 47 ++ tests/unit/data/fiz/su46_v2/303d.json | 50 ++ tests/unit/data/fiz/tem2_v2/tem2a.json | 39 ++ tests/unit/data/fiz/wm15_v1/wm15.json | 22 + tests/unit/data/fiz/wmb10_v1/wmb10.json | 49 ++ tests/unit/data/fiz/z1_v2/152a.json | 14 + tests/unit/data/golden/.gitignore | 4 + tests/unit/drive.cpp | 246 +++++++ tests/unit/drive.h | 56 ++ tests/unit/figures.cpp | 222 ++++++ tests/unit/figures.h | 70 ++ tests/unit/fiz.cpp | 222 ++++++ tests/unit/fiz.h | 44 ++ tests/unit/golden.cpp | 393 +++++++++++ tests/unit/golden.h | 90 +++ tests/unit/invariants.cpp | 163 +++++ tests/unit/invariants.h | 36 + tests/unit/main.cpp | 12 +- tests/unit/real_vehicles.h | 12 + tests/unit/scenario.cpp | 80 +++ tests/unit/scenario.h | 63 ++ tests/unit/test_auxiliaries.cpp | 112 ++++ tests/unit/test_brake_cycle.cpp | 194 ++++++ tests/unit/test_brake_handles.cpp | 154 +++++ tests/unit/test_cab.cpp | 229 +++++++ tests/unit/test_commands.cpp | 547 +++++++++++++++ tests/unit/test_consist.cpp | 135 ++++ tests/unit/test_controls.cpp | 632 ++++++++++++++++++ tests/unit/test_couplers_trains.cpp | 174 +++++ tests/unit/test_definition.cpp | 106 +++ tests/unit/test_diesel.cpp | 179 +++++ tests/unit/test_figures.cpp | 67 ++ tests/unit/test_fiz.cpp | 183 +++++ tests/unit/test_invariants.cpp | 34 + tests/unit/test_loads.cpp | 116 ++++ tests/unit/test_queries.cpp | 61 ++ tests/unit/test_traction.cpp | 270 ++++++++ tests/unit/vehicle.cpp | 254 ++++--- tests/unit/vehicle.h | 70 +- 101 files changed, 8298 insertions(+), 116 deletions(-) create mode 100644 tests/unit/COVERAGE.md create mode 100644 tests/unit/consist.cpp create mode 100644 tests/unit/consist.h create mode 100644 tests/unit/data/fiz/11xa_v2/93a.json create mode 100644 tests/unit/data/fiz/14xa_v1/173a.json create mode 100644 tests/unit/data/fiz/168a_v1/168a.json create mode 100644 tests/unit/data/fiz/21xm_v1/214ma.json create mode 100644 tests/unit/data/fiz/21xm_v1/218ma_a_kd.json create mode 100644 tests/unit/data/fiz/3wl_v2/3wl.json create mode 100644 tests/unit/data/fiz/430w_v2/430w.json create mode 100644 tests/unit/data/fiz/4e_v1/4e.json create mode 100644 tests/unit/data/fiz/README.md create mode 100644 tests/unit/data/fiz/bdhpumn_v3/bdhpumn.json create mode 100644 tests/unit/data/fiz/bhp_v1/bhp-b.json create mode 100644 tests/unit/data/fiz/br285_v1/br285.json create mode 100644 tests/unit/data/fiz/cd/754_v1/754.json create mode 100644 tests/unit/data/fiz/cd/810_v2/810.json create mode 100644 tests/unit/data/fiz/dr2_v1/dr2.json create mode 100644 tests/unit/data/fiz/e186_v2/eu47.json create mode 100644 tests/unit/data/fiz/e6act_v2/e6acta.json create mode 100644 tests/unit/data/fiz/ed72_v1/5bs-ra.json create mode 100644 tests/unit/data/fiz/ed72_v1/5bs-rb.json create mode 100644 tests/unit/data/fiz/ed72_v1/6bs-sa.json create mode 100644 tests/unit/data/fiz/ed72_v1/6bs-sb.json create mode 100644 tests/unit/data/fiz/elf_v1/21we-a.json create mode 100644 tests/unit/data/fiz/elf_v1/21we-b.json create mode 100644 tests/unit/data/fiz/elf_v1/21we-c.json create mode 100644 tests/unit/data/fiz/elf_v1/22wec-a.json create mode 100644 tests/unit/data/fiz/en57_v1/5bgaiii_led_spot.json create mode 100644 tests/unit/data/fiz/en57_v1/5bgb.json create mode 100644 tests/unit/data/fiz/en57_v1/6ba_840.json create mode 100644 tests/unit/data/fiz/en57_v1/6bai.json create mode 100644 tests/unit/data/fiz/en57akl_v1/akl_ra.json create mode 100644 tests/unit/data/fiz/ep05_v2/ep05.json create mode 100644 tests/unit/data/fiz/ep09_v2/104e.json create mode 100644 tests/unit/data/fiz/et22_v3/201e-rn.json create mode 100644 tests/unit/data/fiz/et22_v3/201e-w.json create mode 100644 tests/unit/data/fiz/et42_v2/112e-a.json create mode 100644 tests/unit/data/fiz/et42_v2/112e-b.json create mode 100644 tests/unit/data/fiz/eu04_v1/eu04-01.json create mode 100644 tests/unit/data/fiz/ew58_v1/ew58-d.json create mode 100644 tests/unit/data/fiz/ew58_v1/ew58-sa.json create mode 100644 tests/unit/data/fiz/ew58_v1/ew58-sb.json create mode 100644 tests/unit/data/fiz/impuls_v1/31we-a_pr.json create mode 100644 tests/unit/data/fiz/impuls_v1/31we-b_pr.json create mode 100644 tests/unit/data/fiz/impuls_v1/31we-c_pr.json create mode 100644 tests/unit/data/fiz/impuls_v1/31we-d_pr.json create mode 100644 tests/unit/data/fiz/road/man/nl293.json create mode 100644 tests/unit/data/fiz/sa134_v1/218mc_a.json create mode 100644 tests/unit/data/fiz/sieciowiec_v1/a.json create mode 100644 tests/unit/data/fiz/sm03_v1/sm03.json create mode 100644 tests/unit/data/fiz/sm31_v1/411d.json create mode 100644 tests/unit/data/fiz/sm40_v1/sm40.json create mode 100644 tests/unit/data/fiz/sm42_v1/6d1.json create mode 100644 tests/unit/data/fiz/sn61_v2/sn61.json create mode 100644 tests/unit/data/fiz/st44_v2/m62-1151.json create mode 100644 tests/unit/data/fiz/st45_v2/301dd.json create mode 100644 tests/unit/data/fiz/static/sug/sug_kielce.json create mode 100644 tests/unit/data/fiz/su45_v2/301d.json create mode 100644 tests/unit/data/fiz/su46_v2/303d.json create mode 100644 tests/unit/data/fiz/tem2_v2/tem2a.json create mode 100644 tests/unit/data/fiz/wm15_v1/wm15.json create mode 100644 tests/unit/data/fiz/wmb10_v1/wmb10.json create mode 100644 tests/unit/data/fiz/z1_v2/152a.json create mode 100644 tests/unit/data/golden/.gitignore create mode 100644 tests/unit/drive.cpp create mode 100644 tests/unit/drive.h create mode 100644 tests/unit/figures.cpp create mode 100644 tests/unit/figures.h create mode 100644 tests/unit/fiz.cpp create mode 100644 tests/unit/fiz.h create mode 100644 tests/unit/golden.cpp create mode 100644 tests/unit/golden.h create mode 100644 tests/unit/invariants.cpp create mode 100644 tests/unit/invariants.h create mode 100644 tests/unit/real_vehicles.h create mode 100644 tests/unit/scenario.cpp create mode 100644 tests/unit/scenario.h create mode 100644 tests/unit/test_auxiliaries.cpp create mode 100644 tests/unit/test_brake_cycle.cpp create mode 100644 tests/unit/test_brake_handles.cpp create mode 100644 tests/unit/test_cab.cpp create mode 100644 tests/unit/test_commands.cpp create mode 100644 tests/unit/test_consist.cpp create mode 100644 tests/unit/test_controls.cpp create mode 100644 tests/unit/test_couplers_trains.cpp create mode 100644 tests/unit/test_diesel.cpp create mode 100644 tests/unit/test_figures.cpp create mode 100644 tests/unit/test_fiz.cpp create mode 100644 tests/unit/test_invariants.cpp create mode 100644 tests/unit/test_loads.cpp create mode 100644 tests/unit/test_queries.cpp create mode 100644 tests/unit/test_traction.cpp diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index bb36694bb..199c65a0c 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -2437,7 +2437,7 @@ class TMoverParameters double Adhesive(double staticfriction) const; double TractionForce(double dt); double FrictionForce() const; - double BrakeForceR(double ratio, double velocity); + double BrakeForceR(double ratio, double velocity) const; double BrakeForceP(double press, double velocity) const; double BrakeForce(const TTrackParam &Track); double CouplerForce(int const End, double dt); diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 6b7bdcec5..330e8eb8f 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -5144,7 +5144,7 @@ void TMoverParameters::ComputeTotalForce(double dt) FTotal = FTrain - Sign(V) * FStand; } -double TMoverParameters::BrakeForceR(double ratio, double velocity) +double TMoverParameters::BrakeForceR(double ratio, double velocity) const { double press = 0; if (MBPM > 2) @@ -10482,7 +10482,10 @@ void TMoverParameters::LoadFIZ_Brake(std::string const &line) P2FTrans = 0; } - CntrlPipePress = 5 + 0.001 * (Random(10) - Random(10)); // Ra 2014-07: trochę niedokładności + // Ra 2014-07: trochę niedokładności + // the draws are sequenced: the order of the operands of '-' is unspecified, and GCC and MSVC draw them the other way round + auto const firstdraw{Random(10)}; + CntrlPipePress = 5 + 0.001 * (Random(10) - firstdraw); extract_value(CntrlPipePress, "HiPP", line, ""); HighPipePress = CntrlPipePress; LowPipePress = std::min(HighPipePress, 3.5); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 7bb624106..17efffa7d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -6,20 +6,28 @@ set(UNIT_TESTS_NAME eu07_unit_tests) -# a new test_*.cpp file or data/*.json definition is picked up on its own +# a new test_*.cpp file or file in data/ (definitions, real vehicles, golden traces) is picked up on its own file(GLOB UNIT_TEST_CASES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/test_*.cpp") -file(GLOB UNIT_TEST_DATA CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/data/*.json") +file(GLOB_RECURSE UNIT_TEST_DATA CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/data/*") add_executable(${UNIT_TESTS_NAME} main.cpp vehicle.cpp + fiz.cpp + drive.cpp + consist.cpp + scenario.cpp + golden.cpp + invariants.cpp + figures.cpp engine_stubs.cpp ${UNIT_TEST_CASES}) # the core library brings the game's build settings along target_link_libraries(${UNIT_TESTS_NAME} PRIVATE eu07_core) target_include_directories(${UNIT_TESTS_NAME} PRIVATE "${DEPS_DIR}/doctest") -target_compile_definitions(${UNIT_TESTS_NAME} PRIVATE EU07_UNIT_TESTS_DATA="${CMAKE_CURRENT_SOURCE_DIR}/data") +# the names the tests CAPTURE are often C strings, shown as text rather than pointers +target_compile_definitions(${UNIT_TESTS_NAME} PRIVATE EU07_UNIT_TESTS_DATA="${CMAKE_CURRENT_SOURCE_DIR}/data" DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING) if (MSVC) target_compile_options(${UNIT_TESTS_NAME} PRIVATE /bigobj) endif() diff --git a/tests/unit/COVERAGE.md b/tests/unit/COVERAGE.md new file mode 100644 index 000000000..aafcc27a6 --- /dev/null +++ b/tests/unit/COVERAGE.md @@ -0,0 +1,117 @@ +# Coverage of the simulation core by the unit tests + +These tests characterise the physics of the vehicles (`McZapkie/Mover.cpp`, `hamulce.cpp`, `Oerlikon_ESt.cpp`, `friction.cpp`). They exist to show that refactors of those files, such as splitting `TractionForce` or `RunCommand`, leave the behaviour as it was. This page lists: + +- what the tests reach; +- what nothing in the game ever runs; +- what only data no game vehicle has would run; +- what is left to test; +- the behaviour the tests found that may deserve a look. + +## How it was measured + +Coverage was measured on Windows with MSVC's SanitizerCoverage (`/fsanitize-coverage=trace-pc-guard`) on the unit test binary. The counts are basic blocks, which comes close to branch coverage: both sides of a condition are blocks of their own. The tool is local and not part of the build, by decision (2026-10). Any block-coverage tool run over `eu07_unit_tests` gives comparable numbers. + +| File | Blocks run, at the start | Now | +|---|---|---| +| `Mover.cpp` | 8.9% | 80.7% (3323 of 4118) | +| `hamulce.cpp` | 30% | 76.3% (649 of 851) | +| `Oerlikon_ESt.cpp` | 58% | 87.4% (118 of 135) | +| `friction.cpp` | 9% | 90.9% (10 of 11) | + +The aim is branch coverage with the rest explained below, not 100%. + +## Code nothing in the game runs + +Nothing creates these objects or calls these functions anywhere in the game, which was checked against the whole source tree. A refactor can delete them, or leave them untested. + +- **Brake valves that never get created:** + - `TESt`, `TESt3` and `TESt3AL2`, including `Init`, `PLC` and `SetLP`. The valve names ESt3, ESt3AL2 and ESt4 all create `TNESt3`. + - `TESt4R::GetPF`, overridden by `TLSt` and never called by it. + - `TESt::CheckState`, `CVs`, `BVs`, `GetPF` and `EStParams`, and `TNESt3::EStParams`. +- **`TFV4a`** (`GetPF`, `Init`): the FV4a handle of the vehicle definitions creates `TFV4aM`. +- **Base implementations always overridden:** `TFricMat::GetFC` (every material, the default P10 too, has its own) and `TPrzekladnik::Update` (every relay has its own). +- **Functions without callers:** + - In `hamulce.cpp`: `PF_old`, `TBrake::GetBCF`, `TDriverHandle::OvrldButton`, `TReservoir::pa` and `TBrakeCyl::pa`. + - In `Mover.cpp`: `BrakeVP`, `PipeRatio`, `RealPipeRatio`, `IncBrakeLevel`, `DecBrakeLevel`, `IncBrakePress`, `DecBrakePress`, `MainCtrlActualPowerPos` and `GetExternalCommand`. +- **Inverter failures** of the induction motor drives (`inverter::Failure_Drive`, `Failure_Const`, `Activate`): nothing ever sets them. The cab only shows them. The branches reading them in `TractionForce` can't be reached. + +## Code only data no game vehicle has would run + +Where it was worth it, a test fits the missing parameter to a real vehicle or builds a definition with it (`definition::set()`). Those cases are marked "tested" below. The rest is reachable only by a vehicle definition the game data doesn't have: + +- **Tested, fitted or handmade:** + - The valves CV1, CV1_R, CV1_L_TR, the W, K and Hik variants, SW, NESt3 and M483 (`brake cycle: the valves the game knows…`). + - An Oerlikon valve named with -ED or AL2 without ESt3 (its relays `TPrzekED` and `TPrzekCiagly`). + - The brake materials FR513, FR510, P10yBg and P10yBgu (`brake cycle: every material…`). + - The handles test, D2, Westinghouse, MHZ_EN57 and St113 fitted to an EU07. + - A `CompressorList`, a `PmaxList` and a `Headlights` section. + - The rear cab's compressor limits (`MinCP_B`, `MaxCP_B`). + - The controller of the electrodynamic brake apart from the local brake (`SplitEDPneumaticBrake`). +- **Not tested:** + - The retarder's clutch (`R_IsClutch`, `R_ClutchSpeed`) and the retarder worked by the local brake (`R_WithIndividual`). + - The automatic relays of type 2 (`RelayType=2`). + - The variant of `CheckLocomotiveParameters` for a definition whose `RList` doesn't match its powered axles. + +## Reachable, not tested yet + +These are run by the game, by vehicles it has or by the cab, the AI or the scenery, but no test reaches them yet. Most of them are the far sides of conditions inside functions the tests do run. + +- **Entry points from outside the core:** + - `TBrake::ForceEmptiness` and its overrides: a scenario's vehicle set up with empty brakes (`DynObj.cpp`). + - `TBrake::GetSoundFlag`: the sounds of the brakes. + - `LoadFIZ` and `reload_FIZ`: the file loading around `LoadFIZ_Text`, which the tests call directly. + - `TSecuritySystem::set_cabsignal_lock`. + - `TMHZ_EN57::GetEP`: an MHZ_EN57 handle in the EP mode of the brake. +- **Inside the functions the tests run** (blocks run of the function's total): + - `TractionForce` (305 of 373): the inverter failures above. The rest are mostly halves of conditions: + - the `SST` table of the series-excited diesel-electrics; + - the speed control's brake intervention with the air under `BrakeInterventionVel`; + - a diesel-electric's generator tripping on overcurrent; + - the `ffBrakeList` of the 22WEc, whose electrodynamic brake no test applies. + - `AutoRelayCheck` (87 of 119): the ET22's group switch under some sequences, the relays at reverse with `BackwardsBranchesAllowed`, and the shunting of the EZTs. + - `update_doors` (134 of 160), `PantographsCheck` (44 of 64) and `PowerCouplersCheck` (70 of 88): door modes and pantograph valve kinds only some game vehicles have, and the 24 V circuit across couplers. + - `RelayReset` (29 of 43) and `MainSwitchCheck` (16 of 27): relays tripping in states the drive cycles don't reach. + - `UpdatePipePressure` (108 of 130): the pressure switch lock of an electric unit's pantographs, which the EW58's automatic valve keeps away. Also the EP mode of the EN57's handle and the `LockPipe` thresholds (`LPOn`, `LPOff`), which no vehicle sets. + - `TCV1L_TR::GetPF` (9 of 20), `TKE::GetPF` (28 of 37) and the MHZ handles: settings (G with the CV1L_TR, the KE's anti-slip pressure `ASBP`) that no real vehicle has together. + +## What the tests found + +The tests record the behaviour as it is. The following points may deserve a look by someone who knows how the real vehicles behave. + +- **`Random(10) - Random(10)` (`Mover.cpp`, the noise of the control pipe pressure `CntrlPipePress`):** the order the two calls were evaluated in isn't specified. MSVC Release, MSVC Debug and GCC gave different traces, a control pipe pressure mirrored around 5 bar, which failed 32 tests on the Linux CI. Fixed: the draws are sequenced in the order of MSVC Release, which the golden traces were recorded with. +- **`TCommand::Location` isn't initialised** in a new vehicle. The game always sets it through `PutCommand()`, but a test calling `RunCommand()` directly got different results run to run. +- **`CompressorList` is `int[4][9]`** (`MOVER.h`), though its factors are fractions: 0.9 is read as 0. No game vehicle has the list yet. +- **A train whose pipe leaks under 2.75 bar before its start-up can't be released:** + - Where this happens: an FV4a handle and an LSt valve, for example 4 minutes standing with wagons, waiting for a hot engine. + - The cause: the pipe gets cut off the main reservoir (`lock_old` in `UpdatePipePressure`). The AI presses its unlock button only for the lock set by `LPOn` and `LPOff` (`control_main_pipe`). + - The same after every emergency stop of such a train, by the alarm chain or the radio-stop: reset, it stays with its pipe empty until the releaser is held (`b_rls`), which the test of the emergency stops does. +- **EW58:** + - Its electrodynamic brake comes in only by the stages of the ep brake (TUHEX), never by the brake pipe alone. + - It needs 110 V, from the converter of the middle car only. +- **ET42:** + - At the high current setting the controller stops at the last series position, and the train crawls. + - From the low setting's top speed (over 100 km/h) the dynamic brake's current trips the overload relay at once. +- **EU47:** its local brake sets the flag of the electrodynamic brake, which keeps the train brake's air out, but the motors don't brake. The local brake's air does it. +- **EP09:** the flag of the automatic electrodynamic brake stays on at a standstill while the cylinders hold less than 2.1 bar, and it flicks on while a high application is released. +- **Diesel engines with a second water circuit (SU45, ST44) started at 88 degrees:** they don't start until that circuit, over its limit (74 or 75 degrees), cools. Off, it cools slowly. +- **Brake cylinders over the vehicle's highest pressure** (found by the invariants, see the README): + - The K valve of the SM03 and the EU04 fills the cylinders to the pressure of the auxiliary reservoir in a full service or emergency application: 4.27 bar of the SM03's MaxBP 3.5, 3.19 bar of the EU04's 2.8. + - The ESt3d_PZZ valve of the SN61 does so in an emergency: 4.6 bar of its MaxBP 3.8. + - The EU07's LSt overshoots a little, to 4.0 bar of its 3.8. +- **Diesel engines driven over their top speed:** a lower gear taken at speed drives the engine through the wheels over its `nmax`, the SM03's by 10%, the WMB10's by 25%. The cutoff (`nmax_cutoff`) only takes the fuel off. +- **The ELF's C car's main reservoir over its governor:** the cars share the supply pipe, the A car's governor stops at 9 bar, the C car's at 8.5, so the C car's reservoir reaches 8.64 bar. +- **Wagons thrown about as a train starts with full physics of the couplers:** the slack taken up throws them at up to 5.7 g, and back within a step. +- **Nothing limits the speed:** a light train at full power runs well past its Vmax (the 754 past 160 km/h, against a Vmax of 100). +- **Vehicles that don't do what they should, as driven by the tests:** + - An EN57 doesn't get ready (main reservoir). + - An EN57AKL alone doesn't charge its pipe. + - The TEM2 barely moves, and the SM31 crawls at 0.3 km/h at full power, its motors taking 444 A. + - The draisine can't start from rest (the pump's force is under the resistance of a standing draisine). + - The Elf and the Impuls, their doors opened with the steps permitted, close the doors as they start off but keep the steps out, which keeps them from moving. + - The ED72 holds an ep application with its handle at running, and with the ep brake switched off too; only the ep release position lets it off. The EW58 releases at running. + - The road vehicle (MAN) is only slowed down by a minute of full service. +- **A train run with five physics steps in a frame:** an EU07 with wagons takes no current, its controller at full power, and rolls at a few km/h. The test steps the vehicles the way `TDynamicObject` does (`FastComputeMovement` for the steps before the last one), so the game may do the same. +- **Game data:** + - The 214M's file has `TurboPos` and `MotorParamTable` twice, and the EN57AL's `Engine` twice. The loader reads each in turn, so the later one overrides what both set. + - A wagon set up braked keeps its hand brake on until someone releases it. The AI does that for the whole train (`AutoRewident`), so a test has to as well. diff --git a/tests/unit/README.md b/tests/unit/README.md index 97cc94910..a10f1798e 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -10,6 +10,9 @@ The code under test is `eu07_core`, the library of the simulation parts the game game uses it. The python library gets linked along with the game's settings, but the tests don't use it and run without it. +[COVERAGE.md](COVERAGE.md) says how much of the simulation core the tests reach, what's left and why, and +what they found. + ## Running - **With the build**: building the game (`WITH_TESTS`, on by default) runs them at the end, once the @@ -46,24 +49,127 @@ TEST_CASE("brakes: service braking stops a heavier wagon too") { - `definition::load("")` reads `data/.json`, with the sections and keys of the game's vehicle definitions; `set()` and `remove()` change it for the test. Table sections give their rows as `"rows": [[...], ...]`, keys starting with `_` are comments. A section the loader doesn't - know is an error (the table of them is in `vehicle.cpp`); a misspelt key isn't caught, so check + know is an error (the table of them is in `fiz.cpp`); a misspelt key isn't caught, so check in the test that the value the test is about made it to the vehicle. Keep the definitions physically sensible: some parts need keys others don't (the ESt brake valves need `Brake.Size`), and a test on a half-defined vehicle tests the definition rather than the simulation. +- `definition::fiz("/")` is one of the game's own vehicles, `data/fiz//.json`, + in the same form, converted from the game's `.fiz` with the files it includes (the list and how to + add one: `data/fiz/README.md`). The conversion keeps what the game's loader reads of the file and + checks it reads the same of the converted one (`fiz.h`), so the vehicle is the game's. - The random numbers of the simulation are reseeded before every test case, so a test gives the - same result alone or among the others. + same result alone or among the others. Loading a vehicle draws some too (the scatter of its brake + pipe pressure), so a test making vehicles in a loop calls `test::reseed_random()` before each. - Logic which lives in classes too big to set up in a test (the AI driver, the cab) goes into free functions on the vehicle state the class calls, and the test calls them the same way: see `vehicle/handover.h`, which `TController::TakeControl()` uses, and `test_handover.cpp`. - `vehicle` creates the vehicle the way `TDynamicObject::Init()` does and steps it the way the - simulation loop does, on a straight track of the given friction and gradient. `pair` puts two of - them on that track facing each other with the given ends and runs them together, as the - simulation runs the vehicles of a scene: the coupler and buffer forces, collisions and automatic - coupling come from the physics (`test_couplers.cpp`). Longer consists aren't supported yet. + simulation loop does, on a straight track of the given friction, gradient, catenary voltage and + physics iterations a frame. With a cab (`vehicle{definition, 0.0, "name", 1}`) it has a driver. +- `consist` (`consist.h`) couples vehicles into a train and steps them together, coupler forces, + air, control and power passing between them. The physics reaches a neighbour through its + `TDynamicObject`, for its `MoverParameters` only; the tests have a stand-in of it with just that. +- `drive.h` has the driver's part, made of the AI's steps: `start_up()` (battery, pumps, + pantographs, line breaker, converter, compressor), `drive()` (running, acknowledging the + vigilance), `release_brakes()`, `drive_cycle()` (a whole run from start-up to a stop). +- `scenario` (`scenario.h`) puts a train, its driver and a golden trace for each vehicle together, + for a test of a feature: `act()` marks an event in the traces, `wait()` runs the train. +- `pair` (`vehicle.h`) puts two vehicles on that track facing each other with the given ends and + runs them together, as the simulation runs the vehicles of a scene: the coupler and buffer + forces, collisions and automatic coupling come from the physics (`test_couplers.cpp`). - Check behaviour (speeds, pressures, states the player sees) rather than internal flags, and make the test check its own preconditions (`REQUIRE` that the braking skidded, before checking what a skid leads to). +## Golden traces + +A golden trace pins down how the simulation behaves now, for changes which mustn't alter it, like +restructuring a function: the values of some channels of a vehicle over a run, compared with the +ones recorded from the code before, in `data/golden/.csv`. A difference fails the test, naming +the channel, the time and both rows. + +The samples and their key figures aren't kept in the repository (tens of megabytes, and they only mean +something against the code before the change): record them before the change, and compare after it. + +``` +git switch master +EU07_UNIT_TESTS_UPDATE_GOLDEN=1 eu07_unit_tests # records data/golden/**/*.csv, ignored by git +git switch my-refactor +EU07_UNIT_TESTS_GOLDEN_EXACT=1 eu07_unit_tests # compares every sample, bit for bit +``` + +The recorded files stay through switching branches. Without them, a trace checks the invariants and +its test's own `CHECK`s, which is what the CI does: every test states the behaviour it's about in +`CHECK`s, a scenario of a feature what the feature does (the doors open, the train stops), a run of +every real vehicle what holds for each of them (it brakes harder by every step, it runs off and stops). + +```cpp +test::trace trace{"eu07/notch up", test::channels::motion() + test::channels::traction()}; +eu07.record(trace); // samples every 10th step +eu07->MainSwitch(true); +trace.mark("main switch"); // shows in the event column of the row +eu07.run(30.0); +trace.check(); +``` + +- `EU07_UNIT_TESTS_UPDATE_GOLDEN=1` records the samples and the key figures, for a new test or a + change which alters the behaviour on purpose; the diff of the key figures shows what it altered, + review it like code. +- Without `EU07_UNIT_TESTS_GOLDEN_EXACT=1`, samples recorded are compared within a relative 1e-9, as + GCC and glibc's maths functions can differ from MSVC's in the last bits, and a trace without them + passes. With it they're compared exactly, and a trace without them or its key figures fails, so a + run of the refactoring check can't pass by having nothing to compare. +- Keep the runs short (tens of seconds): the simulation feeds back on itself, and a last bit grows. +- A trace with no assertion about what the behaviour should be is a characterisation, bugs included; + put the behaviour the test is about into `CHECK`s besides. + +### Key figures + +Next to its samples, every trace records the figures a person reads (`figures.h`), for each part of +the run between two events, in `data/golden/.figures`: the duration, the speeds, the distance, +the time to stop, the brake pipe's lowest pressure and the time to get back to 4.9 bar, the peaks of +the brake cylinders, the motor current and the tractive force. + +``` +[brake] +duration = 60.00 s +speed at the start = 118.5 km/h +distance = 944.3 m +stopped after = 45.97 s +brake cylinders peak = 3.997 bar +``` + +They're compared within 0.5% or the last decimal written, loose enough for any compiler, and the +differences are told by part and figure: + +``` +key figures of "traction/4e_v1/4e" differ: + [brake] distance: 944.3 m now, 980.0 m recorded +``` + +`EU07_UNIT_TESTS_UPDATE_GOLDEN=1` records them with the samples. After a change which alters the +behaviour, their comparison says in a few lines what the thousands of changed samples mean. A trace +sampled by hand, at times of its own (the commands and switches tests), records none. + +### Invariants + +Every trace also checks, on every step of its vehicle and not only on the recorded ones, the rules +which hold for any vehicle whatever a test does (`invariants.h`): no NaN, the brake cylinders under +the vehicle's highest pressure, the pipes under the highest governor setting, no motor current with +the line breaker open, no tractive force without power, the distance counted as the speed runs, no +acceleration over 5 g, and a few more. They compare nothing with a recorded file, so they hold on +any compiler, and they still check a behaviour recorded anew on purpose makes sense. A broken rule +fails the test, naming the time, the last event and the values: + +``` +invariant "no motor current with the line breaker open" broken in traction/et42 fuse/0 at 63.200 s, +after "dynamic brake": Mains=0 Im=-412.5 Itot=-412.5 +``` + +Where a test makes a vehicle break a rule (a collision), or a game vehicle breaks one as it is, the +list of excuses in `invariants.cpp` names the trace, the rule and the reason; the second kind goes +into the findings of [COVERAGE.md](COVERAGE.md) too. + ## Code to test, and when the tests fail to link A source joins the tested code by moving from `SOURCES` to `CORE_SOURCES` in the main diff --git a/tests/unit/consist.cpp b/tests/unit/consist.cpp new file mode 100644 index 000000000..a5f10d936 --- /dev/null +++ b/tests/unit/consist.cpp @@ -0,0 +1,106 @@ +#include "stdafx.h" +#include "consist.h" + +#include "vehicle/DynObj.h" + +namespace test { + +consist::consist() = default; +consist::~consist() = default; + +vehicle &consist::add(definition const &Definition, int Cab, int Coupling, double Velocity, double Gap) { + auto added{std::make_unique(Definition, Velocity, std::format("test{}", m_vehicles.size()), Cab)}; + auto &mover{added->mover()}; + + std::vector object(sizeof(TDynamicObject)); + reinterpret_cast(object.data())->MoverParameters = &mover; // NOSONAR see m_objects + + auto position{0.0}; + if (false == m_vehicles.empty()) { + auto &last{m_vehicles.back()->mover()}; + position = m_positions.back() - 0.5 * (last.Dim.L + mover.Dim.L) - Gap; + mover.Loc = TLocation{position, 0.0, 0.0}; + if (Coupling != coupling::faux) { + last.Attach(end::rear, end::front, &mover, Coupling, true, false); + mover.Attach(end::front, end::rear, &last, Coupling, true, false); + } + } + else { + mover.Loc = TLocation{position, 0.0, 0.0}; + } + m_vehicles.push_back(std::move(added)); + m_positions.push_back(position); + m_objects.push_back(std::move(object)); + update_neighbours(); + return *m_vehicles.back(); +} + +void consist::update_neighbours() { + // TDynamicObject::update_neighbours(), for vehicles coupled in a line + for (std::size_t i = 0; i < m_vehicles.size(); ++i) { + auto &mover{m_vehicles[i]->mover()}; + for (int end = end::front; end <= end::rear; ++end) { + auto &neighbour{mover.Neighbours[end]}; + auto const other{end == end::front ? static_cast(i) - 1 : static_cast(i) + 1}; + if (other < 0 || other >= static_cast(m_vehicles.size())) { + neighbour = neighbour_data(); + continue; + } + auto const &othermover{m_vehicles[other]->mover()}; + auto const otherend{end == end::front ? end::rear : end::front}; + auto const distance{TMoverParameters::CouplerDist(&mover, &othermover) - mover.Couplers[end].adapter_length - + othermover.Couplers[otherend].adapter_length}; + // a vehicle not coupled is a collision source when it's within the range the game scans + if (mover.Couplers[end].Connected == nullptr && distance > std::max(10.0, std::abs(mover.V)) + 40.0) { + neighbour = neighbour_data(); + continue; + } + neighbour.vehicle = reinterpret_cast(m_objects[other].data()); // NOSONAR see m_objects + neighbour.vehicle_end = otherend; + neighbour.distance = static_cast(distance); + } + } +} + +void consist::step(track const &Track, double Dt) { + for (auto const &vehicle : m_vehicles) { + vehicle->begin_step(); + } + update_neighbours(); + for (int iteration = 1; iteration < Track.iterations; ++iteration) { + for (auto const &vehicle : m_vehicles) { + vehicle->forces(Dt); + } + for (std::size_t i = 0; i < m_vehicles.size(); ++i) { + m_positions[i] += m_vehicles[i]->fast_move(Track, Dt, TLocation{m_positions[i], 0.0, 0.0}); + } + } + for (auto const &vehicle : m_vehicles) { + vehicle->forces(Dt); + } + // the average pressure in the train's brake pipe weighed by the vehicles' lengths, TDynamicObject::GetEPP(), + // over the whole train (the game's stops at a vehicle without the brake hose coupled) + auto pipe{0.0}; + auto length{0.0}; + for (auto const &vehicle : m_vehicles) { + pipe += vehicle->mover().PipePress * vehicle->mover().Dim.L; + length += vehicle->mover().Dim.L; + } + pipe /= length; + for (std::size_t i = 0; i < m_vehicles.size(); ++i) { + m_positions[i] += m_vehicles[i]->move(Track, Dt, TLocation{m_positions[i], 0.0, 0.0}, pipe); + } +} + +void consist::run(double Seconds, track const &Track, double Dt) { + auto const steps{std::lround(Seconds / (Dt * Track.iterations))}; + for (long i = 0; i < steps; ++i) { + step(Track, Dt); + } +} + +double consist::gap(std::size_t Index) const { + return TMoverParameters::CouplerDist(&m_vehicles[Index]->mover(), &m_vehicles[Index + 1]->mover()); +} + +} // namespace test diff --git a/tests/unit/consist.h b/tests/unit/consist.h new file mode 100644 index 000000000..02001e042 --- /dev/null +++ b/tests/unit/consist.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include "vehicle.h" + +class TDynamicObject; + +namespace test { + +// vehicles coupled into a train on a straight track, all facing forward, from the front one back, stepped the +// way vehicle_table::update() steps the vehicles of the game: the constants and neighbours of all of them, +// their forces, then their movement. couplers carry the forces, the air, the control and the power between +// the vehicles as in the game: +// test::consist en57; +// en57.add(definition::fiz("en57_v1/5bgb"), 1); // driven from the front cab +// en57.add(definition::fiz("en57_v1/6ba_840")); +// en57.add(definition::fiz("en57_v1/5bgb")); +class consist { +public: + // everything a coupler can join, as two vehicles of a train coupled by a crew + static constexpr int full_coupling{coupling::coupler | coupling::brakehose | coupling::mainhose | coupling::control | coupling::gangway | coupling::heating | + coupling::permanent | coupling::power110v | coupling::power24v | coupling::highvoltage}; + + consist(); + ~consist(); + consist(consist const &) = delete; + consist &operator=(consist const &) = delete; + + // a vehicle at the back of the train, its front coupler joined to the rear one of the last vehicle with + // the given coupling (coupling flags), Gap [m] between their buffers. Cab as for a vehicle. a vehicle + // with no coupling (coupling::faux) is free on the same track, as one about to be coupled or hit + vehicle &add(definition const &Definition, int Cab = 0, int Coupling = full_coupling, double Velocity = 0.0, double Gap = 0.0); + + vehicle &operator[](std::size_t Index) { return *m_vehicles[Index]; } + std::size_t size() const { return m_vehicles.size(); } + + void step(track const &Track = {}, double Dt = 0.01); + void run(double Seconds, track const &Track = {}, double Dt = 0.01); + + // distance between the couplers of the vehicle and the one behind it [m], negative when the buffers press + double gap(std::size_t Index) const; + +private: + void update_neighbours(); + + std::vector> m_vehicles; + std::vector m_positions; // of the vehicles' middles along the track + // the physics reaches a neighbouring vehicle through its TDynamicObject, for its MoverParameters alone. + // the game's object can't be made without the rest of the game, so it's a stand-in: storage of its size + // with that one member set, never constructed, which only the physics ever looks into + std::vector> m_objects; +}; + +} // namespace test diff --git a/tests/unit/data/fiz/11xa_v2/93a.json b/tests/unit/data/fiz/11xa_v2/93a.json new file mode 100644 index 000000000..63dbb4fbb --- /dev/null +++ b/tests/unit/data/fiz/11xa_v2/93a.json @@ -0,0 +1,14 @@ +{ + "_source": "pkp/11xa_v2/93a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "40000", "Vmax": "120", "HeatingP": "32", "LightP": "0.56"}, + "Load": {"MaxLoad": "8", "LoadQ": "pieces", "LoadAccepted": "Workers", "OverLoadFactor": "1.5", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "24.5", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.50", "Bd": "17.2", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "ESt4", "Size": "200", "NBpA": "4", "MBF": "90", "MaxBP": "4.0", "BCN": "2", "BCR": "0.1778", "BCD": "0.150", "BCM": "8.17", "BCS": "1.4", "BSA": "4.0", "BM": "P10-Bgu", "BVV": "200", "BRE": "0.87"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "115"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "R", "ASB": "Yes", "MCPN": "1", "rows": []}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "495", "AlterLRS": "Generator"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "Mixed", "OpenSpeed": "70", "CloseSpeed": "400", "DoorMaxShiftL": "80", "DoorMaxShiftR": "80", "DoorOpenMethod": "Fold", "DoorClosureWarning": "No"} +} diff --git a/tests/unit/data/fiz/14xa_v1/173a.json b/tests/unit/data/fiz/14xa_v1/173a.json new file mode 100644 index 000000000..d1243f7ed --- /dev/null +++ b/tests/unit/data/fiz/14xa_v1/173a.json @@ -0,0 +1,14 @@ +{ + "_source": "pkp/14xa_v1/173a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "42000", "Mred": "1600", "Vmax": "160", "HeatingP": "42", "LightP": "0.56"}, + "Load": {"MaxLoad": "30", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "1.5", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "24.5", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.70", "Bd": "17.2", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "ESt4HBG300-s216", "Size": "375", "NBpA": "4", "MBF": "90", "MaxBP": "3.8", "BCN": "2", "BCR": "0.2032", "BCD": "0.150", "BCM": "9.04", "BCS": "1.6", "BSA": "4.0", "BM": "P10-Bgu", "BVV": "375", "BRE": "0.95"}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "AutomaticCtrl", "OpenSpeed": "0.36", "CloseSpeed": "0.48", "DoorCloseDelay": "2.2", "DoorMaxShiftL": "0.9", "DoorMaxShiftR": "0.9", "DoorOpenMethod": "Plug", "DoorClosureWarning": "yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.043", "PlatformMaxShift": "-45", "PlatformSpeed": "4", "PlatformOpenMethod": "Rotate", "DoorStayOpen": "10"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "115"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "R", "ASB": "Yes", "MCPN": "1", "rows": []}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "495", "AlterLRS": "Generator"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"} +} diff --git a/tests/unit/data/fiz/168a_v1/168a.json b/tests/unit/data/fiz/168a_v1/168a.json new file mode 100644 index 000000000..aa391639a --- /dev/null +++ b/tests/unit/data/fiz/168a_v1/168a.json @@ -0,0 +1,14 @@ +{ + "_source": "pkp/168a_v1/168a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "42000", "Mred": "1600", "Vmax": "160", "HeatingP": "20", "LightP": "0.56"}, + "Load": {"MaxLoad": "56", "LoadQ": "pieces", "LoadAccepted": "Passengers,Luggage", "OverLoadFactor": "1.5", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "24.5", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.70", "Bd": "17.2", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "ESt4HBG300", "Size": "200", "NBpA": "4", "MBF": "90", "MaxBP": "3.8", "BCN": "2", "BCR": "0.1778", "BCD": "0.150", "BCM": "10.00", "BCS": "1.4", "BSA": "6.0", "BM": "P10-Bgu", "BVV": "200", "BRE": "0.9"}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "AutomaticCtrl", "OpenSpeed": "0.44", "CloseSpeed": "0.27", "DoorCloseDelay": "2.25", "DoorMaxShiftL": "0.9", "DoorMaxShiftR": "0.9", "DoorOpenMethod": "Plug", "DoorClosureWarning": "yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.043", "PlatformMaxShift": "-45", "PlatformSpeed": "4", "PlatformOpenMethod": "Rotate", "DoorStayOpen": "10"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "115"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "R", "ASB": "Yes", "MCPN": "1", "rows": []}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "495", "AlterLRS": "Generator"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"} +} diff --git a/tests/unit/data/fiz/21xm_v1/214ma.json b/tests/unit/data/fiz/21xm_v1/214ma.json new file mode 100644 index 000000000..10cfcd60c --- /dev/null +++ b/tests/unit/data/fiz/21xm_v1/214ma.json @@ -0,0 +1,78 @@ +{ + "_source": "pkp/21xm_v1/214ma.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "46000", "Vmax": "120", "PWR": "390", "SandCap": "100", "Type": "dmu"}, + "Load": {"MaxLoad": "50", "OverLoadFactor": "2.2", "LoadQ": "pieces", "LoadAccepted": "passengers", "LoadSpeed": "0.5", "UnLoadSpeed": "0.7"}, + "Dimensions": {"L": "24.38", "H": "4.2", "W": "2.9", "Cx": "0.0"}, + "Wheels": {"D": "0.84", "Tw": "1.435", "AIM": "300", "Axle": "B'2'", "Ad": "2", "Bd": "17.23", "Rmin": "75", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "Size": "150", "NBpA": "4", "MBF": "2.74", "MaxBP": "2.9", "LocMaxBP": "3.8", "RV": "-1", "RM": "0.77", "BCN": "8", "BCR": "0.127", "BCD": "0.1", "BCM": "2.15", "HiPP": "5", "LoPP": "3.5", "Vv": "0.125", "MinCP": "7.5", "MaxCP": "8.5", "CompressorSpeed": "0.0138", "CompressorPower": "Main", "BCS": "0.5", "BSA": "0", "BM": "Cosid", "BVV": "88", "BRE": "0.97", "TareMaxBP": "2.2", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "2", "UBB2": "8"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.25", "CloseSpeed": "0.19", "DoorCloseDelay": "0.1", "DoorMaxShiftL": "0.78", "DoorMaxShiftR": "0.78", "DoorOpenMethod": "Plug", "DoorClosureWarning": "No", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.04", "MirrorMaxShift": "75", "DoorStayOpen": "10", "DoorClosexDelay": "1.8", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "No"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "ControlType": "214MA"}, + "SpringBrake": {"Volume": "20", "MBF": "5.8", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "0.5", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "3", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Yes", "LocalBrake": "PneumaticBrake", "ManualBrake": "No", "MCPN": "5", "SCPN": "5", "AutoRelay": "Yes", "IniCDelay": "1.0", "SCDelay": "0.3", "BrakeDelays": "PR", "MaxBPMass": "55", "BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "WaterStart": "Battery", "OilStart": "Automatic", "FuelStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "4", "BrakeOpModes": "PN", "EIMCtrlType": "3", "DirChangeMaxPos": "4", "SBD": "0.47", "AutomaticCabActivation": "No", "InactiveCabFlag": "27", "CompartmentLightsStart": "Manual", "rows": [ + ["-1","0.6","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "EmergencyBrakeDelay": "2.1", "RadioStop": "Yes", "CabDependent": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "28", "LMaxCurrent": "500"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "696", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "1500", "HGeneratorMaxVoltage": "3300"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "50:129", "TransEff": "0.91", "nmin": "800", "nmax": "2000", "nmax_cutoff": "1950", "AIM": "65", "EUS": "102.5", "EDS": "102.0", "IsTC": "Yes", "TC_TMMax": "1.82", "TC_CP": "0.75", "TC_LT": "3000", "TC_LR": "1", "TC_ULR": "1", "TC_FRI": "1", "TC_FRD": "2", "TC_TII": "2.93", "TC_TIO": "0", "TC_TOO": "-1.65", "TC_LS": "29.5", "TC_ULS": "40", "IsRetarder": "Yes", "R_Place": "1", "R_TII": "5.0", "R_MT": "1500", "R_MP": "250", "R_FRI": "1", "R_FRD": "1", "R_MinVel": "30", "WaterMaxTemperature": "105", "WaterCoolingTemperature": "90", "WaterFlowTemperature": "35", "WaterAuxCircuit": "Yes"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|30|40|50|60|70|80|90|100|120", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "120", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "Yes", "PowerUpSpeed": "0.3", "PowerDownSpeed": "0.3"}, + "TurboPos": {"TurboPos": "1"}, + "MotorParamTable": {"minVelfullengage": "3.0", "engageDia": "1", "engageMaxForce": "3000", "engagefriction": "1", "rows": []}, + "MotorParamTable0": {"minVelfullengage": "3.0", "engageDia": "1", "engageMaxForce": "3000", "engagefriction": "1", "rows": [ + ["0","0","0","0","-1","-1","0","1"], + ["1","2.81","-1","-1","38.2","42.4","0.18","1"], + ["2","1.84","17.4","29.9","53.8","59.8","0.13","1"], + ["3","1.36","26.6","45.0","70.8","78.7","0","1"], + ["4","1.00","61.1","61.1","90.4","100.4","0","1"], + ["5","0.80","80.2","80.2","200","200","0","1"] + ]}, + "DList": {"Size": "5", "Mmax": "2450", "nMmax": "21.7", "nmax": "33", "Mnmax": "2160", "nominalfill": "1.0", "Mstand": "250.0", "NomFuelConsRate": "220", "rows": [ + ["0","0.15","0"], + ["1","0.20","2"], + ["2","0.40","2"], + ["3","0.60","2"], + ["4","0.80","2"], + ["5","1.00","2"] + ]}, + "DMList": {"rows": [ + ["0","0"], + ["80","637"], + ["400","796"], + ["800","1592"], + ["1050","2090"], + ["1300","2200"], + ["1550","2124"], + ["1800","2070"], + ["2000","1676"] + ]}, + "UCList": {"Size": "5", "IntegratedBrakePN": "Yes", "IntegratedBrake": "Yes", "rows": [ + ["0","3","0","0","0","0.15","0.15","0","1","0"], + ["1","2","0","0","0","0.15","0.15","1","3","0"], + ["2","1","-1","0","-1","0.15","0.15","3","3","1"], + ["3","1","-1","1","0","0.00","0.15","3","4","1"], + ["4","0","-1","1","0","0.15","0.00","4","5","3"], + ["5","0","0","1","1","0.15","0.15","4","5","3"] + ]}, + "LightsList": {"Size": "5", "Wrap": "no", "Default": "1", "rows": [ + ["0","0"], + ["21","34"], + ["22","34"], + ["34","34"], + ["16","1"] + ]}, + "WiperList": {"Size": "5", "Angle": "75", "Default": "1", "rows": [ + ["3","0.5","0.5","0.0"], + ["0","0.8","0.0","0.5"], + ["3","0.8","6.0","0.5"], + ["3","0.8","3.0","0.0"], + ["3","0.8","1.0","0.0"] + ]} +} diff --git a/tests/unit/data/fiz/21xm_v1/218ma_a_kd.json b/tests/unit/data/fiz/21xm_v1/218ma_a_kd.json new file mode 100644 index 000000000..6d2f9db61 --- /dev/null +++ b/tests/unit/data/fiz/21xm_v1/218ma_a_kd.json @@ -0,0 +1,84 @@ +{ + "_source": "pkp/21xm_v1/218ma_a_kd.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "38500", "Vmax": "120", "PWR": "360", "SandCap": "100", "Type": "dmu"}, + "Load": {"MaxLoad": "50", "OverLoadFactor": "2.2", "LoadQ": "pieces", "LoadAccepted": "passengers", "LoadSpeed": "0.5", "UnLoadSpeed": "0.7"}, + "Dimensions": {"L": "20.75", "H": "4.2", "W": "2.9", "Cx": "0.10"}, + "Wheels": {"D": "0.84", "Tw": "1.435", "AIM": "300", "Axle": "B'1'", "Ad": "2", "Bd": "17.23", "Rmin": "75", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "Size": "150", "NBpA": "4", "MBF": "2.74", "MaxBP": "2.9", "LocMaxBP": "3.8", "RV": "-1", "RM": "0.77", "BCN": "6", "BCR": "0.127", "BCD": "0.1", "BCM": "2.15", "HiPP": "5", "LoPP": "3.5", "Vv": "0.125", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.015", "CompressorPower": "Main", "BCS": "0.5", "BSA": "0", "BM": "Cosid", "BVV": "88", "BRE": "0.97", "TareMaxBP": "2.2", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "2", "UBB2": "8"}, + "SpringBrake": {"Volume": "15", "MBF": "4.35", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "0.5", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.25", "CloseSpeed": "0.19", "DoorCloseDelay": "0.1", "DoorMaxShiftL": "0.78", "DoorMaxShiftR": "0.78", "DoorOpenMethod": "Plug", "DoorClosureWarning": "No", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.04", "MirrorMaxShift": "75", "DoorStayOpen": "10", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "No"}, + "BuffCoupl1": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39"}, + "BuffCoupl2": {"CType": "Articulated", "AllowedFlag": "-55"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "3", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Yes", "LocalBrake": "PneumaticBrake", "ManualBrake": "No", "MCPN": "5", "SCPN": "5", "AutoRelay": "Yes", "IniCDelay": "1.0", "SCDelay": "0.3", "BrakeDelays": "PR", "MaxBPMass": "55", "BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "WaterStart": "Battery", "OilStart": "Automatic", "FuelStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "4", "BrakeOpModes": "PN", "EIMCtrlType": "3", "DirChangeMaxPos": "4", "SBD": "0.47", "AutomaticCabActivation": "No", "InactiveCabFlag": "27", "CompartmentLightsStart": "Manual", "rows": [ + ["-1","0.6","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.1", "RadioStop": "Yes", "CabDependent": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "28", "LMaxCurrent": "500"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "50:129", "TransEff": "0.95", "nmin": "800", "nmax": "1800", "nmax_cutoff": "1850", "nreg_acc": "150", "AIM": "30", "EUS": "102.5", "EDS": "102.0", "IsTC": "Yes", "TC_TMMax": "1.82", "TC_CP": "0.85", "TC_LT": "2500", "TC_LR": "1", "TC_ULR": "1", "TC_FRI": "1", "TC_FRD": "2", "TC_TII": "2.93", "TC_TIO": "0", "TC_TOO": "-1.65", "TC_LS": "22", "TC_ULS": "40", "IsRetarder": "Yes", "R_Place": "1", "R_TII": "5.0", "R_MT": "1500", "R_MP": "250", "R_FRI": "1", "R_FRD": "1", "R_MinVel": "1", "WaterMaxTemperature": "105", "WaterCoolingTemperature": "95", "WaterFlowTemperature": "75", "WaterAuxCircuit": "No", "WaterCoolingFanSpeed": "-100", "NominalCoolingPower": "300"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "696", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "1500", "HGeneratorMaxVoltage": "3300"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|30|40|50|60|70|80|90|100|120", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "120", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "Yes", "PowerUpSpeed": "0.17", "PowerDownSpeed": "0.17"}, + "TurboPos": {"TurboPos": "1"}, + "MotorParamTable": {"minVelfullengage": "3.0", "engageDia": "1", "engageMaxForce": "2300", "engagefriction": "1", "rows": []}, + "MotorParamTable0": {"minVelfullengage": "3.0", "engageDia": "1", "engageMaxForce": "2300", "engagefriction": "1", "rows": [ + ["0","0","0","0","-1","-1","0","1"], + ["1","2.81","-1","-1","31.3","33.7","0.17","1"], + ["2","1.84","17.4","29.9","47.9","51.5","0.11","1"], + ["3","1.36","26.6","45.0","64.8","69.7","0","1"], + ["4","1.00","61.1","61.1","84.2","90.8","0","1"], + ["5","0.80","80.2","80.2","200","200","0","1"] + ]}, + "DList": {"Size": "5", "Mmax": "2450", "nMmax": "21.7", "nmax": "30", "Mnmax": "2160", "nominalfill": "1.0", "Mstand": "250.0", "NomFuelConsRate": "220", "rows": [ + ["0","0.15","0"], + ["1","0.20","2"], + ["2","0.40","2"], + ["3","0.60","2"], + ["4","0.80","2"], + ["5","1.00","2"] + ]}, + "DMList": {"rows": [ + ["0","0"], + ["80","637"], + ["400","796"], + ["800","1592"], + ["1050","2090"], + ["1300","2200"], + ["1550","2032"], + ["1800","1910"], + ["2000","1547"] + ]}, + "V2NList": {"rows": [ + ["0","1200"], + ["40","1800"], + ["120","1800"] + ]}, + "UCList": {"Size": "5", "IntegratedBrakePN": "Yes", "IntegratedBrake": "Yes", "rows": [ + ["0","3","0","0","0","0.15","0.15","0","1","0"], + ["1","2","0","0","0","0.15","0.15","1","3","0"], + ["2","1","-1","0","-1","0.15","0.15","3","3","1"], + ["3","1","-1","1","0","0.00","0.15","3","4","1"], + ["4","0","-1","1","0","0.15","0.00","4","5","3"], + ["5","0","0","1","1","0.15","0.15","4","5","3"] + ]}, + "LightsList": {"Size": "5", "Wrap": "no", "Default": "1", "rows": [ + ["0","0"], + ["21","34"], + ["22","34"], + ["34","34"], + ["16","1"] + ]}, + "WiperList": {"Size": "5", "Angle": "75", "Default": "1", "rows": [ + ["3","0.5","0.5","0.0"], + ["0","0.8","0.0","0.5"], + ["3","0.8","6.0","0.5"], + ["3","0.8","3.0","0.0"], + ["3","0.8","1.0","0.0"] + ]} +} diff --git a/tests/unit/data/fiz/3wl_v2/3wl.json b/tests/unit/data/fiz/3wl_v2/3wl.json new file mode 100644 index 000000000..96e22b6ca --- /dev/null +++ b/tests/unit/data/fiz/3wl_v2/3wl.json @@ -0,0 +1,11 @@ +{ + "_source": "pkp/3wl_v2/3wl.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "10500", "Vmax": "100"}, + "Load": {"MaxLoad": "30", "LoadQ": "tonns", "LoadAccepted": "Coal,Sand,sand2,ruda,tluczen,scrap,scrap2", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "9.87", "H": "3", "W": "3", "Cx": "0.1"}, + "Wheels": {"D": "1", "Tw": "1.435", "AIM": "300", "Axle": "1'1'", "Bd": "6", "Rmin": "30", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "ESt3", "Size": "12", "MBF": "72", "MaxBP": "3.8", "BCN": "1", "BCR": "0.1524", "BCD": "0.14", "BCMlo": "3.05", "BCMHi": "10.67", "NBpA": "4", "BM": "P10-Bg", "BCS": "2.7", "BSA": "8.00", "BVV": "50", "BRE": "0.8"}, + "BuffCoupl": {"CType": "Screw", "kB": "9000", "DmaxB": "0.12", "FmaxB": "350", "kC": "4500", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.52", "AllowedFlag": "3"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "GP", "rows": []} +} diff --git a/tests/unit/data/fiz/430w_v2/430w.json b/tests/unit/data/fiz/430w_v2/430w.json new file mode 100644 index 000000000..b99807857 --- /dev/null +++ b/tests/unit/data/fiz/430w_v2/430w.json @@ -0,0 +1,11 @@ +{ + "_source": "pkp/430w_v2/430w.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "20500", "Vmax": "100"}, + "Load": {"MaxLoad": "59.5", "LoadQ": "tonns", "LoadAccepted": "coal,piasek,piasek2,ruda,tluczen,zlom,zlom2,siarka", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "14.028", "H": "3.235", "W": "3.04", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "Tw": "1.435", "AIM": "300", "Axle": "2'2'", "Ad": "2", "Bd": "8.5", "Rmin": "75", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "ESt3AL2HBG300", "Size": "100", "MBF": "144", "MaxBP": "3.8", "TareMaxBP": "1.3", "BCN": "1", "BCR": "0.2032", "BCD": "0.145", "BCM": "12.00", "NBpA": "4", "BM": "P10-Bg", "BCS": "2.7", "BSA": "16.00", "BVV": "100", "BRE": "0.9"}, + "BuffCoupl": {"CType": "Screw", "kB": "9000", "DmaxB": "0.12", "FmaxB": "350", "kC": "4500", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.52", "AllowedFlag": "3"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "GP", "rows": []} +} diff --git a/tests/unit/data/fiz/4e_v1/4e.json b/tests/unit/data/fiz/4e_v1/4e.json new file mode 100644 index 000000000..6781a9e88 --- /dev/null +++ b/tests/unit/data/fiz/4e_v1/4e.json @@ -0,0 +1,81 @@ +{ + "_source": "pkp/4e_v1/4e.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "80000", "Mred": "16000", "Vmax": "125", "PWR": "2000", "SandCap": "500"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "15.915", "H": "4.348", "W": "2.903", "Cx": "0.38"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "3.05", "Bd": "8.55", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.0", "MaxASBP": "1.1", "RM": "0.635", "BCN": "8", "BCR": "0.1143", "BCD": "0.05", "BCM": "5.18", "Vv": "0.828", "MinCP": "7.0", "MaxCP": "8.7", "BCS": "1", "BSA": "0.5", "BM": "P10-Bgu", "BVV": "278", "CompressorSpeed": "0.0566", "BRE": "0.9"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103", "ControlType": "4e"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "7", "BDelay3": "39", "BDelay4": "17", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "43", "SCPN": "6", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.26", "SCDDelay": "0.05", "BrakeDelays": "GPR", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "MotorBlowersStart": "Converter", "rows": [ + ["-1","0.6","-1","12","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.43","-1","9","Pneumatic"], + ["2","0.40","-1","9","Pneumatic"], + ["3","0.37","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0","-1","18","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareMinSpeed": "10", "AwareDelay": "60", "SoundSignalDelay": "3", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "4", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1250", "MinH": "0.48", "MaxH": "2.5", "CSW": "1.4", "MinV": "1900", "MinPress": "3.5", "MaxPress": "5.0", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "18:79", "WindingRes": "0.14", "nmax": "2390", "MotorBlowersSpeed": "-1310"}, + "MotorParamTable0": {"rows": [ + ["0","21.8","105","0.11","140.9","105","0.11"], + ["1","20.22","140","0.07","130.7","140","0.07"], + ["2","21.06","190","0.09","136.2","190","0.09"], + ["3","23.95","160","0.25","154.8","160","0.25"], + ["4","21.73","255","0.16","140.5","255","0.16"], + ["5","24.66","170","0.35","159.4","170","0.35"], + ["6","23","800","0.02","148.7","800","0.02"] + ]}, + "Circuit": {"CircuitRes": "0.5", "ImaxLo": "600", "ImaxHi": "750", "IminLo": "350", "IminHi": "470"}, + "RList": {"Size": "43", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.2", "rows": [ + ["0","0","0","0","0"], + ["1","24.891","1","4","0"], + ["2","16.821","1","4","0"], + ["3","12.301","1","4","0"], + ["4","9.771","1","4","0"], + ["5","8.111","1","4","0"], + ["6","6.915","1","4","0"], + ["7","6.481","1","4","0"], + ["8","6.047","1","4","0"], + ["9","5.629","1","4","0"], + ["10","5.211","1","4","0"], + ["11","4.827","1","4","0"], + ["12","4.443","1","4","0"], + ["13","4.099","1","4","0"], + ["14","3.755","1","4","0"], + ["15","3.442","1","4","0"], + ["16","3.129","1","4","0"], + ["17","2.838","1","4","0"], + ["18","2.547","1","4","0"], + ["19","2.286","1","4","0"], + ["20","2.025","1","4","0"], + ["21","1.780","1","4","0"], + ["22","1.535","1","4","0"], + ["23","1.275","1","4","0"], + ["24","1.095","1","4","0"], + ["25","0.775","1","4","0"], + ["26","0.525","1","4","0"], + ["27","0.275","1","4","0"], + ["28","0.000","1","4","0"], + ["29","5.44","2","2","0"], + ["30","4.41","2","2","0"], + ["31","3.85","2","2","0"], + ["32","3.29","2","2","0"], + ["33","2.82","2","2","0"], + ["34","2.36","2","2","0"], + ["35","1.93","2","2","0"], + ["36","1.56","2","2","0"], + ["37","1.21","2","2","0"], + ["38","0.89","2","2","0"], + ["39","0.61","2","2","0"], + ["40","0.34","2","2","0"], + ["41","0.21","2","2","0"], + ["42","0.10","2","2","0"], + ["43","0.00","2","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/README.md b/tests/unit/data/fiz/README.md new file mode 100644 index 000000000..514fb6287 --- /dev/null +++ b/tests/unit/data/fiz/README.md @@ -0,0 +1,66 @@ +# Real vehicle definitions + +The game's own vehicles (`dynamic/` of the MaSzyna release used for the tests, 2026-10), to test the simulation on the vehicles players drive. Chosen to cover every engine type, brake valve, brake handle, local brake, coupler type, door type and brake delay set the game data uses. `pkp/` is left out of the paths, the other operators keep theirs. + +They aren't the game's files: each `.json` here holds what the game's vehicle loader reads of the `.fiz`, with the files it includes, in the form of the tests' own definitions (`vehicle.h`): the values of every section line, and the rows of the lists. `test_fiz.cpp` converts them, using the game's parser for the includes, and checks the loader reads the same of the converted definition as of the game's file (`fiz.h`): the same sections with the same values, the same rows read by the same lists, in the same order. Comments, and lines the loader skips, are left out. + +Convert them again, from the game data in `/dynamic`, and add vehicles (paths as in the table, without the extension, separated by commas), with: + + EU07_UNIT_TESTS_FIZ_SOURCE=/dynamic EU07_UNIT_TESTS_FIZ_ADD=sm42_v1/6d1 eu07_unit_tests -tc="fiz: conversion*" + +Every `.json` here is a vehicle picked up by `test_fiz.cpp`: record the parameters of an added one with `EU07_UNIT_TESTS_UPDATE_GOLDEN=1`. + +| Path | Vehicle | Engine | Brake valve | Brake handle | Local brake | Coupler | +|---|---|---|---|---|---|---| +| `11xa_v2/93a.fiz` | 11XA | - | ESt4 | - | ManualBrake | Screw | +| `14xa_v1/173a.fiz` | 14XA | - | ESt4HBG300-s216 | - | ManualBrake | Screw | +| `168a_v1/168a.fiz` | 168A | - | ESt4HBG300 | - | ManualBrake | Screw | +| `21xm_v1/218ma_a_kd.fiz` | 218M | DieselEngine | EStED | MHZ_K5P | PneumaticBrake | Screw | +| `21xm_v1/214ma.fiz` | 214M | DieselEngine | EStED | MHZ_K5P | PneumaticBrake | Screw | +| `3wl_v2/3wl.fiz` | 3WL | - | ESt3 | - | ManualBrake | Screw | +| `430w_v2/430w.fiz` | 430W | - | ESt3AL2HBG300 | - | ManualBrake | Screw | +| `4e_v1/4e.fiz` | 4E | ElectricSeriesMotor | LSt | FV4a | PneumaticBrake | Screw | +| `bdhpumn_v3/bdhpumn.fiz` | BDHPUMN | - | KE | - | ManualBrake | Screw | +| `bhp_v1/bhp-b.fiz` | BHP | - | ESt3AL2 | - | ManualBrake | Articulated | +| `br285_v1/br285.fiz` | BR285 | DieselElectric | KE | MHZ_6P | PneumaticBrake | Screw | +| `cd/810_v2/810.fiz` | 810 | DieselEngine | ESt3_PZZ | FV4a | PneumaticBrake | Screw | +| `cd/754_v1/754.fiz` | 754 | DumbDE | LSt | FV4a | PneumaticBrake | Screw | +| `dr2_v1/dr2.fiz` | DR2 | WheelsDriven | - | - | ManualBrake | None | +| `e186_v2/eu47.fiz` | E186 | ElectricInductionMotor | EStED | MHZ_6P | PneumaticBrake | Screw | +| `e6act_v2/e6acta.fiz` | E6ACT | ElectricInductionMotor | EStED | MHZ_K5P | PneumaticBrake | Screw | +| `ed72_v1/5bs-rb.fiz` | ED72 | - | EP2 | FVel6 | ManualBrake | Automatic | +| `elf_v1/22wec-a.fiz` | ELF 22WEc | ElectricInductionMotor | EStED | MHZ_K5P | PneumaticBrake | Automatic | +| `elf_v1/21we-a.fiz` | ELF | ElectricInductionMotor | EStED | MHZ_K5P | PneumaticBrake | Automatic | +| `en57_v1/5bgb.fiz` | EN57 | - | EP2 | FVel6 | ManualBrake | Automatic | +| `en57_v1/6ba_840.fiz` | EN57 | - | EP2 | FVel6 | ManualBrake | Automatic | +| `en57_v1/6bai.fiz` | EN57 | - | K | St113 | ManualBrake | Automatic | +| `en57akl_v1/akl_ra.fiz` | EN57AKL | ElectricInductionMotor | EStED | MHZ_EN57 | PneumaticBrake | Automatic | +| `ep05_v2/ep05.fiz` | EP05 | ElectricSeriesMotor | LSt | FV4a | PneumaticBrake | Screw | +| `ep09_v2/104e.fiz` | EP09 | ElectricSeriesMotor | EStED | FV4a | PneumaticBrake | Screw | +| `et22_v3/201e-rn.fiz` | ET22 | ElectricSeriesMotor | LSt | FV4a | PneumaticBrake | Screw | +| `et22_v3/201e-w.fiz` | ET22 | ElectricSeriesMotor | LSt | FV4a | PneumaticBrake | Screw | +| `et42_v2/112e-a.fiz` | ET42 | ElectricSeriesMotor | LSt | FV4a | PneumaticBrake | Screw | +| `et42_v2/112e-b.fiz` | ET42 | ElectricSeriesMotor | LSt | FV4a | PneumaticBrake | Screw | +| `eu04_v1/eu04-01.fiz` | EU04 | ElectricSeriesMotor | K | Knorr | PneumaticBrake | Screw | +| `ew58_v1/ew58-d.fiz` | EW58 | - | EP1 | - | ManualBrake | Automatic | +| `ew58_v1/ew58-sa.fiz` | EW58 | ElectricSeriesMotor | EP1 | FVE408 | ManualBrake | Automatic | +| `impuls_v1/31we-a_pr.fiz` | IMPULS | ElectricInductionMotor | EStED | MHZ_6P | PneumaticBrake | Automatic | +| `road/man/nl293.fiz` | MAN (road) | DieselEngine | - | - | HydraulicBrake | Bare | +| `sa134_v1/218mc_a.fiz` | SA134 | DieselEngine | EStED | MHZ_K5P | PneumaticBrake | Screw | +| `sieciowiec_v1/a.fiz` | SIECIOWIEC | - | - | - | NoBrake | Screw | +| `sm03_v1/sm03.fiz` | SM03 | DieselEngine | K | Knorr | PneumaticBrake | Screw | +| `sm31_v1/411d.fiz` | SM31 | DumbDE | LSt | FV4a | PneumaticBrake | Screw | +| `sm40_v1/sm40.fiz` | SM40 | DieselElectric | K | Knorr | PneumaticBrake | Screw | +| `sm42_v1/6d1.fiz` | SM42 | DumbDE | LSt | FV4a | PneumaticBrake | Screw | +| `sn61_v2/sn61.fiz` | SN61 | DieselEngine | ESt3d_PZZ | FV4a | PneumaticBrake | Screw | +| `st44_v2/m62-1151.fiz` | ST44 | DieselElectric | LSt | FV4a | PneumaticBrake | Screw | +| `st45_v2/301dd.fiz` | ST45 | DumbDE | SW | MHZ_K8P | PneumaticBrake | Screw | +| `static/sug/sug_kielce.fiz` | SUG | ElectricSeriesMotor | W | Knorr | PneumaticBrake | Screw | +| `su45_v2/301d.fiz` | SU45 | DumbDE | LSt | FV4a | PneumaticBrake | Screw | +| `su46_v2/303d.fiz` | SU46 | DumbDE | LSt | FV4a | PneumaticBrake | Screw | +| `tem2_v2/tem2a.fiz` | TEM2 | DieselElectric | LSt | M394 | PneumaticBrake | Screw | +| `wm15_v1/wm15.fiz` | WM15 | Dumb | LSt | FV4a | PneumaticBrake | Screw | +| `wmb10_v1/wmb10.fiz` | WMB10 | DieselEngine | LSt | FV4a | PneumaticBrake | Screw | +| `z1_v2/152a.fiz` | Z1 | - | KE | - | ManualBrake | Screw | + +The paths are those of the game's `.fiz` files. `elf_v1/inverter.fiz` is a part the Elf units include, not a vehicle. The 218M (V2NList), the SA134 (HTCList) and the 22WEc (ffBrakeList) are there for lists the others lack, the EP05 for its camshaft. The 214MA stands for the 214M railbus, whose own file repeats a section (TurboPos and MotorParamTable twice), which the definitions here can't hold. diff --git a/tests/unit/data/fiz/bdhpumn_v3/bdhpumn.json b/tests/unit/data/fiz/bdhpumn_v3/bdhpumn.json new file mode 100644 index 000000000..80b1ab662 --- /dev/null +++ b/tests/unit/data/fiz/bdhpumn_v3/bdhpumn.json @@ -0,0 +1,14 @@ +{ + "_source": "pkp/bdhpumn_v3/bdhpumn.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "42200", "Vmax": "120", "HeatingP": "40", "LightP": "0.56"}, + "Load": {"MaxLoad": "132", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "26.8", "H": "4.7", "W": "2.88", "Cx": "0.15"}, + "Wheels": {"D": "0.96", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.5", "Bd": "19.5", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "KE", "NBpA": "6", "MBF": "15", "MaxBP": "3.8", "BCN": "12", "BCR": "0.127", "BCD": "0.04", "BCM": "1.5", "BCS": "1.2", "BSA": "0", "BM": "Disk1", "BVV": "125", "TareMaxBP": "2.3"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "Mixed", "OpenSpeed": "0.5", "CloseSpeed": "1.0", "DoorCloseDelay": "0", "DoorMaxShiftL": "0.65", "DoorMaxShiftR": "0.65", "DoorOpenMethod": "Shift", "DoorMaxShiftPlug": "0.028", "DoorStayOpen": "5.0", "DoorClosureWarning": "no", "DoorBlocked": "Yes", "DoorAutoCloseRemote": "No", "DoorNeedPermit": "no"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "115"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "P", "MaxBPMass": "63", "rows": []}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "495", "AlterLRS": "Generator"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000", "AlterHeating": "PowerCable", "AlterHPowerTrans": "SteamPower", "AlterHSteamPress": "0.45"} +} diff --git a/tests/unit/data/fiz/bhp_v1/bhp-b.json b/tests/unit/data/fiz/bhp_v1/bhp-b.json new file mode 100644 index 000000000..031542506 --- /dev/null +++ b/tests/unit/data/fiz/bhp_v1/bhp-b.json @@ -0,0 +1,15 @@ +{ + "_source": "pkp/bhp_v1/bhp-b.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "36000", "Vmax": "120", "HeatingP": "37.5", "LightP": "0.56"}, + "Load": {"LoadAccepted": "Passengers,Luggage", "MaxLoad": "110", "LoadQ": "pieces", "OverLoadFactor": "2", "LoadSpeed": "3", "UnLoadSpeed": "4"}, + "Dimensions": {"L": "19.75", "H": "4.750", "W": "2.720", "Cx": "0.4"}, + "Wheels": {"D": "0.94", "AIM": "300", "Tw": "1.435", "Axle": "2'3'", "Ad": "3.0", "Bd": "17", "Rmin": "80", "BearingType": "Roll"}, + "BuffCoupl1": {"CType": "Articulated", "AllowedFlag": "83", "PowerFlag": "256"}, + "BuffCoupl2": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "67"}, + "Brake": {"BrakeValve": "ESt3AL2", "Size": "100", "MBF": "144", "MaxBP": "3.8", "TareMaxBP": "2.5", "BCN": "2", "BCR": "0.1403", "BCD": "0.14", "BCM": "10.38", "NBpA": "4", "BM": "P10-Bgu", "BCS": "2.7", "BSA": "5.00", "BVV": "100", "BRE": "0.85"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "P", "MaxBPMass": "55", "rows": []}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "Mixed", "OpenSpeed": "0.55", "CloseSpeed": "0.75", "DoorMaxShiftL": "0.65", "DoorMaxShiftR": "0.65", "DoorOpenMethod": "Shift"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000", "AlterHeating": "PowerCable", "AlterHPowerTrans": "SteamPower", "AlterHSteamPress": "0.45"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "375", "AlterLRS": "Generator"} +} diff --git a/tests/unit/data/fiz/br285_v1/br285.json b/tests/unit/data/fiz/br285_v1/br285.json new file mode 100644 index 000000000..ec7dcbf4d --- /dev/null +++ b/tests/unit/data/fiz/br285_v1/br285.json @@ -0,0 +1,69 @@ +{ + "_source": "pkp/br285_v1/br285.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "82400", "Mred": "12000", "Vmax": "140", "PWR": "2200", "SandCap": "700"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "18.900", "H": "3.95", "W": "2.977", "Cx": "0.1"}, + "Wheels": {"D": "1.250", "AIM": "4000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "1.75", "Bd": "10.3", "Rmin": "80"}, + "SpringBrake": {"Volume": "10", "MBF": "8", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "3.5", "ValveOnArea": "0.0002", "ValveOffArea": "0.0002", "ValvePNBArea": "0.002", "MTC": "4"}, + "BuffCoupl": {"ControlType": "br285", "CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103"}, + "Brake": {"BrakeValve": "KE", "NBpA": "4", "MBF": "50", "MaxBP": "3.8", "BCN": "8", "RM": "1.4", "BCR": "0.127", "BCD": "0.04", "BCM": "2.7", "BCS": "0.8", "BSA": "0", "BM": "Disk2", "BVV": "125", "Vv": "0.8", "CompressorSpeed": "0.04", "CompressorPower": "Converter", "MinCP": "8", "MaxCP": "10", "UBB3": "8", "LPOn": "3.0", "LPOff": "4.5", "HandlePipeUnlockPos": "2", "EmergencyCutsOffHandle": "Yes"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "3", "BDelay3": "35", "BDelay4": "36", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "6", "SCPN": "4", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GPR", "BrakeHandle": "MHZ_6P", "LocBrakeHandle": "FD1", "ConverterStart": "Manual", "ConverterStartDelay": "3", "FuelStart": "Automatic", "OilStart": "Automatic", "BrakeOpModes": "PN", "EIMCtrlType": "3", "DirChangeMaxPos": "3", "HAO": "No", "HMO": "Yes", "HGDP1": "2", "rows": [ + ["-1","0.8","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal,SeparateAcknowledge", "AwareDelay": "30", "SoundSignalDelay": "2.5", "MaxHoldTime": "1.0", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "100"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "600", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "1860", "HGeneratorMaxVoltage": "3300"}, + "SpeedControl": {"SpeedCtrl": "Yes", "OverrideManual": "No", "InitPwr": "1.0", "MaxPwrVel": "-1", "StartVel": "-1", "VelStep": "5", "PwrStep": "0.01", "MinPwr": "0.0", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "140", "Offset": "0", "kPpos": "0.1", "kPneg": "0.2", "kIpos": "0.03", "kIneg": "0.03", "BrakeIntervention": "No", "SpeedCtrlType": "Time", "SpeedCtrlDelay": "0.5"}, + "Engine": {"EngineType": "DieselElectric", "Trans": "18:64", "Ftmax": "300000", "Vhyp": "24", "Vadd": "5.5", "Cr": "1", "WaterMinTemperature": "40", "WaterFlowTemperature": "70", "WaterCoolingTemperature": "82", "WaterMaxTemperature": "91", "WaterShutters": "Yes", "WaterAuxCircuit": "Yes", "WaterAuxCoolingTemperature": "50", "WaterAuxMaxTemperature": "74", "WaterAuxShutters": "Yes", "OilMinPressure": "0.15", "HeatKFO2": "33"}, + "MotorParamTable": {"rows": [ + ["0","8.35","50","2000","48.2","0","0"] + ]}, + "Switches": {"Converter": "impulse"}, + "WWList": {"Size": "6", "rows": [ + ["600","0","0","0"], + ["800","600","1404","6000"], + ["950","900","1404","6000"], + ["1050","1100","1404","6000"], + ["1250","1500","1404","6000"], + ["1450","1850","1404","6000"], + ["1860","2188","1404","6000"] + ]}, + "LightsList": {"Size": "12", "Wrap": "Yes", "Default": "1", "rows": [ + ["21","34"], + ["34","0"], + ["37","34"], + ["21","34"], + ["38","34"], + ["22","34"], + ["17","17"], + ["17","17"], + ["42","42"], + ["4","4"], + ["17","17"], + ["16","1"] + ]}, + "WiperList": {"Size": "4", "Angle": "70", "rows": [ + ["0","0.8","0.0","0.5"], + ["3","0.8","5.0","0.5"], + ["3","0.8","0.5","0.0"], + ["3","0.6","0.5","0.0"], + ["endL"] + ]}, + "UCList": {"Size": "6", "IntegratedBrakePN": "no", "IntegratedLocBrake": "no", "IntegratedBrake": "no", "rows": [ + ["0","0","-1","0","-1","0.15","0.15","0","1","1"], + ["1","0","-1","0","0","0.00","0.00","1","3","1"], + ["2","0","-1","0","0","0.15","0.00","1","3","1"], + ["3","0","0","0","0","0.00","0.00","1","5","3"], + ["4","0","0","1","0","0.00","0.15","3","5","5"], + ["5","0","0","1","0","0.00","0.00","3","6","5"], + ["6","0","0","1","1","0.15","0.15","5","6","5"] + ]} +} diff --git a/tests/unit/data/fiz/cd/754_v1/754.json b/tests/unit/data/fiz/cd/754_v1/754.json new file mode 100644 index 000000000..4f0a850a9 --- /dev/null +++ b/tests/unit/data/fiz/cd/754_v1/754.json @@ -0,0 +1,56 @@ +{ + "_source": "cd/754_v1/754.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "74500", "Mred": "15000", "Vmax": "100", "PWR": "1460", "SandCap": "400", "HeatingP": "300"}, + "Dimensions": {"L": "16.54", "H": "4.335", "W": "2.900", "Cx": "-0.1"}, + "Wheels": {"D": "1.000", "AIM": "4000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.4", "Bd": "9.0", "Rmin": "100"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.1", "LocMaxBP": "4.5", "MaxASBP": "1.5", "RM": "0.69", "BCN": "8", "BCR": "0.114", "BCD": "0.06", "BCM": "8.1", "HiPP": "5", "LoPP": "3.5", "Vv": "0.8", "MinCP": "7.0", "MaxCP": "8.5", "CompressorSpeed": "0.069", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "CompressorPower": "Converter", "BVV": "240", "BRE": "0.9"}, + "BuffCoupl": {"ControlType": "cd754", "CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103"}, + "Cntrl": {"BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "6", "BDelay3": "36", "BDelay4": "32", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "14", "SCPN": "3", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GPR", "rows": [ + ["-1","0.7","-1","15","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.29","-1","9","Pneumatic"], + ["6","0","-1","15","Pneumatic"] + ]}, + "LightsList": {"Size": "8", "Wrap": "No", "Default": "1", "rows": [ + ["0","0"], + ["388","34"], + ["21","34"], + ["1","16"], + ["16","1"], + ["22","34"], + ["38","38"], + ["34","34"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "227.8"}, + "Engine": {"EngineType": "DumbDE", "Trans": "18:62", "Ftmax": "215000", "Flat": "0", "Vhyp": "7.2", "Vadd": "5.5", "Cr": "1.0", "RelayType": "0"}, + "Clima": {"Heating": "Main"}, + "MotorParamTable": {"rows": [ + ["0","20","100","3000","20.8","1340","1713"], + ["1","18.5","110","2700","25","1340","1713"], + ["2","17.2","135","2500","30","1340","1713"], + ["3","14.3","170","2000","48.5","1340","1713"] + ]}, + "WWList": {"Size": "14", "rows": [ + ["500","0","0","0"], + ["500","0","0","0"], + ["550","122","78","273"], + ["550","122","78","273"], + ["600","244","147","547"], + ["650","365","221","820"], + ["700","287","295","1094"], + ["750","609","369","1367"], + ["800","730","442","1641"], + ["850","852","516","1914"], + ["900","973","590","2187"], + ["950","1095","664","2461"], + ["1000","1216","737","2734"], + ["1050","1338","811","3007"], + ["1100","1460","885","3281"] + ]} +} diff --git a/tests/unit/data/fiz/cd/810_v2/810.json b/tests/unit/data/fiz/cd/810_v2/810.json new file mode 100644 index 000000000..89a3c945e --- /dev/null +++ b/tests/unit/data/fiz/cd/810_v2/810.json @@ -0,0 +1,41 @@ +{ + "_source": "cd/810_v2/810.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "Type": "dmu", "M": "20000", "Vmax": "80", "PWR": "155", "SandCap": "25"}, + "Load": {"MaxLoad": "55", "OverLoadFactor": "2.2", "LoadQ": "pieces", "LoadAccepted": "passengers", "LoadSpeed": "0.5", "UnLoadSpeed": "0.7"}, + "Dimensions": {"L": "13.86", "H": "2.8", "W": "3.00", "Cx": "0.38"}, + "Wheels": {"D": "0.84", "Tw": "1.435", "AIM": "300", "Axle": "1'A'", "Bd": "8", "Rmin": "100", "BearingType": "Roll"}, + "Brake": {"BrakeType": "Oerlikon", "BrakeValve": "ESt3_PZZ", "Size": "100", "NBpA": "4", "MBF": "45.0", "MaxBP": "3.8", "BCN": "2", "BCR": "0.127", "BCD": "0.14", "BCM": "3.25", "HiPP": "5.0", "LoPP": "3.5", "Vv": "0.257", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.0136", "BM": "P10-Bg", "BVV": "75", "BRE": "0.87", "AirLeakRate": "1.15", "CompressorPower": "Converter"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "3"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.35", "CloseSpeed": "0.625", "DoorMaxShiftL": "0.9", "DoorMaxShiftR": "0.9", "DoorOpenMethod": "Plug", "DoorClosureWarning": "No", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.02", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "No"}, + "Cntrl": {"BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "8", "SCPN": "2", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.3", "ManualBrake": "Yes", "WaterStart": "Battery", "OilStart": "Automatic", "FuelStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "4", "CompartmentLightsStart": "Manual", "rows": [ + ["-1","1.0","-1","12","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.43","-1","9","Pneumatic"], + ["2","0.4","-1","9","Pneumatic"], + ["3","0.37","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.3","-1","9","Pneumatic"], + ["6","0","-1","16","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "600", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "2150", "HGeneratorMaxVoltage": "3300"}, + "Security": {"AwareSystem": "Active", "AwareDelay": "30", "SoundSignalDelay": "0.1", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "5", "RadioStop": "No"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "13:51", "nmin": "600", "nmax": "2150", "nmax_cutoff": "2500", "AIM": "60", "EUS": "2.5", "EDS": "2.5", "IsTC": "Yes", "TC_TMMax": "2.70", "TC_CP": "0.85", "TC_LT": "850", "TC_LR": "1", "TC_ULR": "1", "TC_FRI": "1", "TC_FRD": "1", "TC_TII": "0.60", "TC_TIO": "0", "TC_TOO": "0", "TC_LS": "55", "TC_ULS": "45", "IsRetarder": "No", "WaterMaxTemperature": "105", "WaterCoolingTemperature": "90", "WaterFlowTemperature": "75", "WaterAuxCircuit": "No", "WaterCoolingFanSpeed": "-100", "NominalCoolingPower": "154"}, + "MotorParamTable": {"minVelfullengage": "-0.1", "engageDia": "1", "engageMaxForce": "1900", "engagefriction": "1", "rows": [ + ["0","0","0","-1","1"], + ["1","2.120","-1","33","1"], + ["2","1.000","25","200","1"] + ]}, + "DList": {"Size": "8", "Mmax": "850", "nMmax": "29", "nmax": "41", "Mnmax": "750", "nominalfill": "0.9", "Mstand": "100.0", "rows": [ + ["0","0.1","0"], + ["1","0.2","1"], + ["2","0.3","2"], + ["3","0.4","2"], + ["4","0.5","2"], + ["5","0.6","2"], + ["6","0.7","2"], + ["7","0.8","2"], + ["8","1.0","2"] + ]} +} diff --git a/tests/unit/data/fiz/dr2_v1/dr2.json b/tests/unit/data/fiz/dr2_v1/dr2.json new file mode 100644 index 000000000..f80e6ef75 --- /dev/null +++ b/tests/unit/data/fiz/dr2_v1/dr2.json @@ -0,0 +1,13 @@ +{ + "_source": "pkp/dr2_v1/dr2.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "300", "Vmax": "40", "PWR": "6"}, + "Dimensions": {"L": "1.65", "H": "0.79", "W": "1.47", "Cx": "0.058"}, + "Load": {"MaxLoad": "3", "LoadQ": "pieces", "LoadAccepted": "Passengers", "LoadSpeed": "0.5", "UnLoadSpeed": "0.5"}, + "Wheels": {"D": "0.4", "AIM": "4.8", "Tw": "1.435", "Axle": "1'A", "Bd": "0.86", "Rmin": "20", "BearingType": "Slide"}, + "BuffCoupl": {"CType": "None"}, + "Brake": {"BrakeType": "Individual", "MBF": "0.2"}, + "Cntrl": {"BrakeSystem": "Individual", "LocalBrake": "ManualBrake", "MCPN": "1", "SCDelay": "0.2", "rows": []}, + "Power": {"EnginePower": "Internal", "PowerType": "BioPower"}, + "Engine": {"EngineType": "WheelsDriven", "Trans": "9:2", "Ftmax": "1200"} +} diff --git a/tests/unit/data/fiz/e186_v2/eu47.json b/tests/unit/data/fiz/e186_v2/eu47.json new file mode 100644 index 000000000..c20e0c823 --- /dev/null +++ b/tests/unit/data/fiz/e186_v2/eu47.json @@ -0,0 +1,61 @@ +{ + "_source": "pkp/e186_v2/eu47.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "81000", "Mred": "9000", "Vmax": "160", "PWR": "5600", "SandCap": "760"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "18.900", "H": "3.95", "W": "2.977", "Cx": "0.1"}, + "Wheels": {"D": "1.250", "AIM": "4000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "1.75", "Bd": "10.3", "Rmin": "100"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "50", "MaxBP": "3.8", "BCN": "8", "RM": "1.4", "BCR": "0.127", "BCD": "0.04", "BCM": "2.7", "BCS": "0.8", "BSA": "0", "BM": "Disk2", "BVV": "125", "Vv": "0.8", "CompressorSpeed": "0.04", "CompressorPower": "Converter", "MinCP": "8", "MaxCP": "10", "UBB3": "8", "LPOn": "3.0", "LPOff": "4.5", "HandlePipeUnlockPos": "2", "EmergencyCutsOffHandle": "Yes"}, + "SpringBrake": {"Volume": "10", "MBF": "8", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "3.5", "ValveOnArea": "0.0002", "ValveOffArea": "0.0002", "ValvePNBArea": "0.002", "MTC": "4"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103", "ControlType": "EU47"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BrakeOpModes": "PN", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Automatic", "LocalBrake": "PneumaticBrake", "MCPN": "7", "SCPN": "4", "AutoRelay": "No", "MainInitTime": "10", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GPR", "BrakeHandle": "MHZ_6P", "LocBrakeHandle": "FD1", "ScndS": "Yes", "SBD": "0", "ConverterStart": "Automatic", "ConverterStartDelay": "10", "PantCompressorStart": "Automatic", "PantAutoValve": "Yes", "EIMCtrlType": "1", "HAO": "No", "HMO": "No", "HGDP1": "0.0", "HGDP2": "1", "OMP": "0.4", "DirChangeMaxPos": "3", "LocalBrakeTraxx": "Yes", "PantEPValveStart": "Manual", "PantEPValveSpring": "No", "rows": [ + ["-1","0.8","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal,SeparateAcknowledge", "AwareDelay": "30", "SoundSignalDelay": "2.5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "5", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "4", "MaxVoltage": "3900", "MaxCurrent": "2000", "MinH": "0.48", "MinV": "1900", "MaxH": "2.5", "CSW": "1.5", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricInductionMotor", "Trans": "48:251", "dfic": "861", "dfmax": "1.84", "p": "2", "cfu": "43.7", "cim": "13.4", "icif": "0.679", "Uzmax": "2183", "Uzh": "2183", "DU": "20", "I0": "20", "fcfu": "43.7", "F0": "300", "a1": "0.4", "Pmax": "5600", "Fh": "150", "Ph": "2600", "Vh0": "5", "Vh1": "10", "Imax": "1950", "abed": "1", "Flat": "Yes"}, + "Switches": {"Pantograph": "Impulse"}, + "SpeedControl": {"SpeedCtrl": "Yes", "OverrideManual": "No", "InitPwr": "1.0", "MaxPwrVel": "-1", "StartVel": "-1", "VelStep": "5", "PwrStep": "0.01", "MinPwr": "0.0", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "160", "Offset": "0", "kPpos": "0.1", "kPneg": "0.2", "kIpos": "0.03", "kIneg": "0.03", "BrakeIntervention": "No", "SpeedCtrlType": "Time", "SpeedCtrlDelay": "0.5"}, + "Doors": {"DoorPermitList": "0|3", "DoorPermitListDefault": "1"}, + "Circuit": {"ImaxHi": "500"}, + "ffList": {"Size": "7", "rows": [ + ["0","0.4"], + ["6.3","0.4"], + ["7.1","0.6"], + ["25.6","0.6"], + ["26.4","0.7"], + ["29.6","0.7"], + ["30.4","1.0"], + ["60.8","2.0"] + ]}, + "LightsList": {"Size": "12", "Wrap": "Yes", "Default": "1", "rows": [ + ["21","34"], + ["34","0"], + ["37","34"], + ["21","34"], + ["38","34"], + ["22","34"], + ["17","17"], + ["17","17"], + ["42","42"], + ["4","4"], + ["17","17"], + ["16","1"] + ]}, + "WiperList": {"Size": "4", "Angle": "70", "rows": [ + ["0","0.8","0.0","0.5"], + ["3","0.8","5.0","0.5"], + ["3","0.8","0.5","0.0"], + ["3","0.6","0.5","0.0"], + ["endL"] + ]} +} diff --git a/tests/unit/data/fiz/e6act_v2/e6acta.json b/tests/unit/data/fiz/e6act_v2/e6acta.json new file mode 100644 index 000000000..1904bbcf6 --- /dev/null +++ b/tests/unit/data/fiz/e6act_v2/e6acta.json @@ -0,0 +1,41 @@ +{ + "_source": "pkp/e6act_v2/e6acta.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "115000", "Mred": "9000", "Vmax": "120", "PWR": "5004", "SandCap": "700"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "20.33", "H": "4.325", "W": "3.0", "Cx": "0.1"}, + "Wheels": {"D": "1.250", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "3.9", "Bd": "14.2", "Rmin": "150", "BearingType": "Roll"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "35"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "50", "MaxBP": "4.4", "BCN": "12", "MaxLBP": "4.4", "RM": "1.4", "BCR": "0.127", "BCD": "0.04", "BCM": "2.7", "BCS": "0.8", "BSA": "0", "BRE": "0.97", "BM": "P10-Bgu", "BVV": "100", "Vv": "0.8", "CompressorSpeed": "0.04", "MinCP": "8", "MaxCP": "10", "DynamicBrake": "Automatic", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "8", "UBB2": "1", "HandlePipeUnlockPos": "0", "EmergencyCutsOffHandle": "Yes"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Automatic", "LocalBrake": "PneumaticBrake", "MCPN": "10", "SCPN": "12", "AutoRelay": "No", "MainInitTime": "5", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GP", "BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "ConverterStart": "Automatic", "ConverterStartDelay": "10", "PantCompressorStart": "Mixed", "PantAutoValve": "Yes", "LocalBrakeTraxx": "Yes", "HAO": "No", "HMO": "No", "HGDP1": "0.0", "HGDP2": "1", "BrakeOpModes": "PN", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "15", "rows": [ + ["-1","0.8","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes"}, + "Circuit": {"ImaxHi": "500"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "4600", "MaxCurrent": "1600", "MinH": "0.8", "MaxH": "1.8", "CSW": "1.5", "MinV": "2000", "InsetV": "2000", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricInductionMotor", "Trans": "54:253", "dfic": "1021", "dfmax": "0.92", "p": "2", "cfu": "48.9", "cim": "15", "icif": "0.664", "Uzmax": "2150", "Uzh": "2150", "DU": "20", "I0": "20", "fcfu": "48.9", "F0": "420", "a1": "1.53", "Pmax": "5100", "Fh": "200", "Ph": "3000", "Vh0": "5", "Vh1": "10", "Imax": "1870", "abed": "1", "eped": "10"}, + "SpeedControl": {"SpeedCtrl": "Yes", "OverrideManual": "No", "InitPwr": "1.0", "MaxPwrVel": "-1", "StartVel": "-1", "VelStep": "10", "PwrStep": "0.01", "MinPwr": "0.0", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "120", "Offset": "-0.1", "kPpos": "0.2", "kPneg": "0.4", "kIpos": "0.02", "kIneg": "0.00", "BrakeIntervention": "No", "SpeedCtrlDelay": "0.5"}, + "ffList": {"Size": "2", "rows": [ + ["0","0.40"], + ["19.6","0.805"], + ["25.7","1.055"] + ]}, + "LightsList": {"Size": "7", "Wrap": "no", "Default": "4", "rows": [ + ["16","1"], + ["21","34"], + ["22","34"], + ["0","0"], + ["34","34"], + ["17","34"], + ["38","34"] + ]} +} diff --git a/tests/unit/data/fiz/ed72_v1/5bs-ra.json b/tests/unit/data/fiz/ed72_v1/5bs-ra.json new file mode 100644 index 000000000..0f11e0b42 --- /dev/null +++ b/tests/unit/data/fiz/ed72_v1/5bs-ra.json @@ -0,0 +1,25 @@ +{ + "_source": "pkp/ed72_v1/5bs-ra.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "35500", "Vmax": "110", "PWR": "0", "SandCap": "0", "Type": "ezt"}, + "Load": {"MaxLoad": "54", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "1"}, + "Dimensions": {"L": "20.87", "H": "4", "W": "2.9", "Cx": "0.2"}, + "Wheels": {"D": "0.94", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4", "BCN": "1", "BCR": "0.203", "BCD": "0.15", "BCM": "9.39", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.013", "TareMaxBP": "2.5", "BVV": "88", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler2"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.4", "CloseSpeed": "0.4", "DoorCloseDelay": "0.5", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"}, + "BuffCoupl1": {"ControlType": "ed72", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "BuffCoupl2": {"ControlType": "ed72", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-119"}, + "Cntrl": {"BrakeHandle": "FVel6", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "52", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "3", "SCPN": "3", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "ScndS": "Yes", "BatteryStart": "Disabled", "ConverterStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"} +} diff --git a/tests/unit/data/fiz/ed72_v1/5bs-rb.json b/tests/unit/data/fiz/ed72_v1/5bs-rb.json new file mode 100644 index 000000000..ccb89b269 --- /dev/null +++ b/tests/unit/data/fiz/ed72_v1/5bs-rb.json @@ -0,0 +1,25 @@ +{ + "_source": "pkp/ed72_v1/5bs-rb.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "35500", "Vmax": "110", "PWR": "0", "SandCap": "0", "Type": "ezt"}, + "Load": {"MaxLoad": "54", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "1"}, + "Dimensions": {"L": "20.87", "H": "4", "W": "2.9", "Cx": "0.2"}, + "Wheels": {"D": "0.94", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4", "BCN": "1", "BCR": "0.203", "BCD": "0.15", "BCM": "9.39", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.013", "TareMaxBP": "2.5", "BVV": "88", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler1"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.315", "CloseSpeed": "0.325", "DoorMaxShiftL": "0.625", "DoorCloseDelay": "1.6", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"}, + "BuffCoupl1": {"ControlType": "ed72", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-119"}, + "BuffCoupl2": {"ControlType": "ed72", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "Cntrl": {"BrakeHandle": "FVel6", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "52", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "3", "SCPN": "3", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "ScndS": "Yes", "ConverterStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"} +} diff --git a/tests/unit/data/fiz/ed72_v1/6bs-sa.json b/tests/unit/data/fiz/ed72_v1/6bs-sa.json new file mode 100644 index 000000000..bd115fe6c --- /dev/null +++ b/tests/unit/data/fiz/ed72_v1/6bs-sa.json @@ -0,0 +1,47 @@ +{ + "_source": "pkp/ed72_v1/6bs-sa.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "55500", "Vmax": "110", "PWR": "608", "SandCap": "500", "Type": "ezt"}, + "Load": {"MaxLoad": "76", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.77", "H": "4.12", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "1.0", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.75", "Bd": "15.6", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.1", "BCN": "2", "BCR": "0.178", "BCD": "0.15", "BCM": "7.32", "HiPP": "5", "LoPP": "3.5", "Vv": "0.1", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.0", "TareMaxBP": "2.8", "BVV": "100", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "4", "BRE": "0.9"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.38", "CloseSpeed": "0.475", "DoorCloseDelay": "2.0", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"}, + "BuffCoupl1": {"ControlType": "ed72", "CType": "Automatic", "kB": "19200", "DmaxB": "0.05", "FmaxB": "740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-119"}, + "BuffCoupl2": {"ControlType": "ed72", "CType": "Automatic", "kB": "19200", "DmaxB": "0.05", "FmaxB": "740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "Cntrl": {"BrakeSystem": "ElectroPneumatic", "MaxBPMass": "75", "BCPN": "1", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "DynamicBrake": "Passive", "MCPN": "3", "SCPN": "3", "AutoRelay": "Yes", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "ScndS": "Yes", "BatteryStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","-1","-1","0","ElectroPneumatic"], + ["1","-1","0.35","4","ElectroPneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "3600", "MaxCurrent": "1200", "MinH": "0.38", "MinV": "1900", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "1"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "19:70", "WindingRes": "0.3836", "nmax": "2390"}, + "Switches": {"Pantograph": "Impulse"}, + "MotorParamTable0": {"rows": [ + ["0","21.52","81","-0.02","148","81","-0.02","1"], + ["1","21.68","120","-0.02","148.2","120","-0.02","1"], + ["2","21.17","130","0.01","142.3","130","0.01","1"], + ["3","22.46","70","0.24","150.1","70","0.24","0"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "280", "ImaxHi": "281", "IminLo": "135", "IminHi": "175"}, + "RList": {"Size": "14", "RVent": "No", "rows": [ + ["0","0","0","0","0"], + ["1","17.682","1","4","0"], + ["2","14.182","1","4","1"], + ["2","10.682","1","4","1"], + ["2","8.474","1","4","1"], + ["2","6.266","1","4","1"], + ["2","4.605","1","4","1"], + ["2","2.944","1","4","1"], + ["2","1.472","1","4","1"], + ["2","0","1","4","1"], + ["3","8.841","2","2","1"], + ["3","5.341","2","2","1"], + ["3","3.133","2","2","1"], + ["3","1.472","2","2","1"], + ["3","0","2","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/ed72_v1/6bs-sb.json b/tests/unit/data/fiz/ed72_v1/6bs-sb.json new file mode 100644 index 000000000..2a8cb50c1 --- /dev/null +++ b/tests/unit/data/fiz/ed72_v1/6bs-sb.json @@ -0,0 +1,47 @@ +{ + "_source": "pkp/ed72_v1/6bs-sb.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "55500", "Vmax": "110", "PWR": "608", "SandCap": "500", "Type": "ezt"}, + "Load": {"MaxLoad": "76", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.77", "H": "4.12", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "1.0", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.75", "Bd": "15.6", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.1", "BCN": "2", "BCR": "0.178", "BCD": "0.15", "BCM": "7.32", "HiPP": "5", "LoPP": "3.5", "Vv": "0.1", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.0", "TareMaxBP": "2.8", "BVV": "100", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "4", "BRE": "0.9"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.335", "CloseSpeed": "0.34", "DoorCloseDelay": "1.5", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"}, + "BuffCoupl1": {"ControlType": "ed72", "CType": "Automatic", "kB": "19200", "DmaxB": "0.05", "FmaxB": "740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "BuffCoupl2": {"ControlType": "ed72", "CType": "Automatic", "kB": "19200", "DmaxB": "0.05", "FmaxB": "740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-119"}, + "Cntrl": {"BrakeSystem": "ElectroPneumatic", "MaxBPMass": "75", "BCPN": "1", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "DynamicBrake": "Passive", "MCPN": "3", "SCPN": "3", "AutoRelay": "Yes", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "ScndS": "Yes", "BatteryStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","-1","-1","0","ElectroPneumatic"], + ["1","-1","0.35","4","ElectroPneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1200", "MinH": "0.38", "MinV": "1900", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "2"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "19:70", "WindingRes": "0.3836", "nmax": "2390"}, + "Switches": {"Pantograph": "Impulse"}, + "MotorParamTable0": {"rows": [ + ["0","21.52","81","-0.02","148","81","-0.02","1"], + ["1","21.68","120","-0.02","148.2","120","-0.02","1"], + ["2","21.17","130","0.01","142.3","130","0.01","1"], + ["3","22.46","70","0.24","150.1","70","0.24","0"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "280", "ImaxHi": "281", "IminLo": "135", "IminHi": "175"}, + "RList": {"Size": "14", "RVent": "No", "rows": [ + ["0","0","0","0","0"], + ["1","17.682","1","4","0"], + ["2","14.182","1","4","1"], + ["2","10.682","1","4","1"], + ["2","8.474","1","4","1"], + ["2","6.266","1","4","1"], + ["2","4.605","1","4","1"], + ["2","2.944","1","4","1"], + ["2","1.472","1","4","1"], + ["2","0","1","4","1"], + ["3","8.841","2","2","1"], + ["3","5.341","2","2","1"], + ["3","3.133","2","2","1"], + ["3","1.472","2","2","1"], + ["3","0","2","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/elf_v1/21we-a.json b/tests/unit/data/fiz/elf_v1/21we-a.json new file mode 100644 index 000000000..19b3938c6 --- /dev/null +++ b/tests/unit/data/fiz/elf_v1/21we-a.json @@ -0,0 +1,52 @@ +{ + "_source": "pkp/elf_v1/21we-a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "28650", "Vmax": "160", "PWR": "800", "SandCap": "500", "Type": "ezt", "HeatingP": "37"}, + "Load": {"MaxLoad": "108", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2.3", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.081", "H": "4.228", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.850", "AIM": "8000", "Tw": "1.435", "Axle": "Bo'", "Ad": "2.83", "Bd": "16.3", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.1", "MaxLBP": "4.2", "RM": "0.74", "RV": "-1", "BCN": "4", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.5", "MinCP": "7.7", "MaxCP": "9.0", "CompressorSpeed": "0.025", "BVV": "150", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "2.6", "CompressorPower": "Converter", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "2", "UBB2": "8"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.24", "CloseSpeed": "0.2", "DoorMaxShiftL": "0.74", "DoorMaxShiftR": "0.74", "DoorOpenMethod": "Plug", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.05", "DoorCloseDelay": "1.8", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "Yes", "MirrorVelClose": "25"}, + "BuffCoupl1": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39", "ControlType": "en62"}, + "BuffCoupl2": {"CType": "Articulated", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-255"}, + "Cntrl": {"BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "37", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "MCPN": "4", "EIMCtrlType": "2", "SCPN": "1", "MainInitTime": "10", "IniCDelay": "1.0", "SCDelay": "0.2", "CoupledCtrl": "No", "SBD": "0.6", "ASB": "Yes", "CompressorStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "10.0", "PantCompressorStart": "Mixed", "PantAutoValve": "Yes", "BrakeDelays": "PR", "BrakeOpModes": "PN", "DirChangeMaxPos": "3", "HAO": "No", "HMO": "Yes", "HGDP1": "0", "ReleaseParkingBySpringBrake": "Yes", "MotorBlowersStart": "Converter", "CompartmentLightsStart": "Manual", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "3", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Circuit": {"ImaxHi": "500"}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "0.1", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes", "EmergencyBrakeWarningSignal": "2", "MagnetLocation": "4.5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main", "HMaxVoltage": "400"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "4000", "MinV": "2000", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "1"}, + "Blending": {"MED_amax": "1.04", "MED_EPVC": "No", "MED_Ncor": "No", "MED_FirstDelayED": "2", "MED_ScndDelayED": "1"}, + "Engine": {"EngineType": "ElectricInductionMotor", "Trans": "40:218", "dfic": "470", "dfmax": "0.790", "p": "2", "cfu": "33.43", "cim": "10.22", "icif": "0.67", "Uzmax": "2340", "Uzh": "2340", "DU": "20", "I0": "20", "fcfu": "33.5", "F0": "75", "a1": "0.0", "Pmax": "980", "Fh": "48", "Ph": "830", "Vh0": "2", "Vh1": "5", "Imax": "377", "abed": "0", "Flat": "Yes", "edep": "1.0", "MotorBlowersSpeed": "-29", "InvNo": "2", "InvCtrCplFlag": "255"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|40|50|60|70|80|100|120|140|160", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "160", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "No", "PowerUpSpeed": "0.3", "PowerDownSpeed": "0.3"}, + "AI": {"Pantstate": "1"}, + "ffList": {"Size": "10", "rows": [ + ["0","2.49"], + ["22.1","2.49"], + ["24.1","2.50"], + ["31.3","2.50"], + ["32.3","2.53"], + ["39.3","3.0"], + ["65","5.0"], + ["91","7.0"], + ["124","9.0"], + ["161","11.7"], + ["177","13.0"] + ]}, + "LightsList": {"Size": "5", "Wrap": "no", "Default": "1", "rows": [ + ["0","0"], + ["21","34"], + ["22","34"], + ["34","34"], + ["16","1"] + ]} +} diff --git a/tests/unit/data/fiz/elf_v1/21we-b.json b/tests/unit/data/fiz/elf_v1/21we-b.json new file mode 100644 index 000000000..ecd26847c --- /dev/null +++ b/tests/unit/data/fiz/elf_v1/21we-b.json @@ -0,0 +1,25 @@ +{ + "_source": "pkp/elf_v1/21we-b.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "50000", "Vmax": "160", "PWR": "0", "SandCap": "500", "Type": "ezt", "HeatingP": "37"}, + "Load": {"MaxLoad": "108", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2.3", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "16.5", "H": "4.12", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.850", "AIM": "670", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "16.3", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "2", "MBF": "85.0", "MaxBP": "3.1", "MaxLBP": "4.2", "RM": "0.74", "RV": "-1", "BCN": "8", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.1", "MinCP": "7.7", "MaxCP": "9.0", "CompressorSpeed": "0.0000001", "BVV": "176", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "1.9"}, + "SpringBrake": {"Volume": "20", "MBF": "5.8", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.24", "CloseSpeed": "0.2", "DoorMaxShiftL": "0.74", "DoorMaxShiftR": "0.74", "DoorOpenMethod": "Plug", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.05", "DoorCloseDelay": "1.8", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "Yes"}, + "BuffCoupl1": {"CType": "Articulated", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-255"}, + "BuffCoupl2": {"CType": "Articulated", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-255"}, + "Cntrl": {"BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "74", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "MCPN": "4", "EIMCtrlType": "2", "SCPN": "1", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "No", "SBD": "0.6", "ASB": "Automatic", "CompressorStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "10.15", "BrakeDelays": "PR", "BrakeOpModes": "PN", "ReleaseParkingBySpringBrake": "Yes", "CompartmentLightsStart": "Manual", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "400"} +} diff --git a/tests/unit/data/fiz/elf_v1/21we-c.json b/tests/unit/data/fiz/elf_v1/21we-c.json new file mode 100644 index 000000000..10101e91a --- /dev/null +++ b/tests/unit/data/fiz/elf_v1/21we-c.json @@ -0,0 +1,52 @@ +{ + "_source": "pkp/elf_v1/21we-c.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "28650", "Vmax": "160", "PWR": "800", "SandCap": "500", "Type": "ezt", "HeatingP": "37"}, + "Load": {"MaxLoad": "95", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2.3", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "20.70", "H": "4.228", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.850", "AIM": "8000", "Tw": "1.435", "Axle": "Bo'", "Ad": "2.83", "Bd": "16.3", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.1", "MaxLBP": "4.2", "RM": "0.74", "RV": "-1", "BCN": "4", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.5", "MinCP": "7.0", "MaxCP": "8.5", "CompressorSpeed": "0.025", "BVV": "150", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "2.6", "CompressorPower": "Converter", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "2", "UBB2": "8"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.24", "CloseSpeed": "0.2", "DoorMaxShiftL": "0.74", "DoorMaxShiftR": "0.74", "DoorOpenMethod": "Plug", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.05", "DoorCloseDelay": "1.8", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "Yes", "MirrorVelClose": "25"}, + "BuffCoupl1": {"CType": "Articulated", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-255"}, + "BuffCoupl2": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39", "ControlType": "en62"}, + "Cntrl": {"BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "37", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "MCPN": "4", "EIMCtrlType": "2", "SCPN": "1", "MainInitTime": "10", "IniCDelay": "1.0", "SCDelay": "0.2", "CoupledCtrl": "No", "SBD": "0.6", "ASB": "Yes", "CompressorStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "10.0", "PantCompressorStart": "Mixed", "PantAutoValve": "Yes", "BrakeDelays": "PR", "BrakeOpModes": "PN", "DirChangeMaxPos": "3", "HAO": "No", "HMO": "Yes", "HGDP1": "0", "ReleaseParkingBySpringBrake": "Yes", "MotorBlowersStart": "Converter", "CompartmentLightsStart": "Manual", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "3", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Circuit": {"ImaxHi": "500"}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "0.1", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes", "EmergencyBrakeWarningSignal": "2", "MagnetLocation": "4.5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main", "HMaxVoltage": "400"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "4000", "MinV": "2000", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "2"}, + "Blending": {"MED_amax": "1.04", "MED_EPVC": "No", "MED_Ncor": "No", "MED_FirstDelayED": "2", "MED_ScndDelayED": "1"}, + "Engine": {"EngineType": "ElectricInductionMotor", "Trans": "40:218", "dfic": "470", "dfmax": "0.790", "p": "2", "cfu": "33.43", "cim": "10.22", "icif": "0.67", "Uzmax": "2340", "Uzh": "2340", "DU": "20", "I0": "20", "fcfu": "33.5", "F0": "75", "a1": "0.0", "Pmax": "980", "Fh": "48", "Ph": "830", "Vh0": "2", "Vh1": "5", "Imax": "377", "abed": "0", "Flat": "Yes", "edep": "1.0", "MotorBlowersSpeed": "-29", "InvNo": "2", "InvCtrCplFlag": "255"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|40|50|60|70|80|100|120|140|160", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "160", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "No", "PowerUpSpeed": "0.3", "PowerDownSpeed": "0.3"}, + "AI": {"Pantstate": "2"}, + "ffList": {"Size": "10", "rows": [ + ["0","2.49"], + ["22.1","2.49"], + ["24.1","2.50"], + ["31.3","2.50"], + ["32.3","2.53"], + ["39.3","3.0"], + ["65","5.0"], + ["91","7.0"], + ["124","9.0"], + ["161","11.7"], + ["177","13.0"] + ]}, + "LightsList": {"Size": "5", "Wrap": "no", "Default": "1", "rows": [ + ["0","0"], + ["21","34"], + ["22","34"], + ["34","34"], + ["16","1"] + ]} +} diff --git a/tests/unit/data/fiz/elf_v1/22wec-a.json b/tests/unit/data/fiz/elf_v1/22wec-a.json new file mode 100644 index 000000000..6059b098d --- /dev/null +++ b/tests/unit/data/fiz/elf_v1/22wec-a.json @@ -0,0 +1,54 @@ +{ + "_source": "pkp/elf_v1/22wec-a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "29000", "Vmax": "120", "PWR": "800", "SandCap": "500", "Type": "ezt", "HeatingP": "37"}, + "Load": {"MaxLoad": "108", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2.3", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.081", "H": "4.228", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.850", "AIM": "8000", "Tw": "1.435", "Axle": "Bo'", "Ad": "2.83", "Bd": "16.3", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.1", "MaxLBP": "4.2", "RM": "0.74", "RV": "-1", "BCN": "4", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.5", "MinCP": "7.7", "MaxCP": "9.0", "CompressorSpeed": "0.025", "BVV": "150", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "2.6", "CompressorPower": "Converter", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "2", "UBB2": "8"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.24", "CloseSpeed": "0.2", "DoorMaxShiftL": "0.74", "DoorMaxShiftR": "0.74", "DoorOpenMethod": "Plug", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.05", "DoorCloseDelay": "1.8", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "Yes", "MirrorVelClose": "25"}, + "BuffCoupl1": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "BuffCoupl2": {"CType": "Articulated", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-255"}, + "Cntrl": {"BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "35", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "MCPN": "4", "EIMCtrlType": "2", "SCPN": "1", "MainInitTime": "10", "IniCDelay": "1.0", "SCDelay": "0.2", "CoupledCtrl": "No", "SBD": "0.6", "ASB": "Yes", "CompressorStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "10.15", "BrakeDelays": "PR", "BrakeOpModes": "PN", "DirChangeMaxPos": "3", "HAO": "No", "HMO": "Yes", "HGDP1": "0", "ReleaseParkingBySpringBrake": "Yes", "MotorBlowersStart": "Converter", "CompartmentLightsStart": "Manual", "PantEPValveSpring": "No", "PantCompressorStart": "Automatic", "PantAutoValve": "Yes", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "9", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "0.1", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes", "EmergencyBrakeWarningSignal": "2", "MagnetLocation": "4.5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main", "HMaxVoltage": "400"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "4000", "MinV": "2000", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "1"}, + "Switches": {"PantographPresets": "1|3|2"}, + "Circuit": {"ImaxHi": "500"}, + "Blending": {"MED_amax": "1.04", "MED_EPVC": "No", "MED_Ncor": "No", "MED_FirstDelayED": "2", "MED_ScndDelayED": "1"}, + "Engine": {"EngineType": "ElectricInductionMotor", "Trans": "40:218", "dfic": "470", "dfmax": "0.790", "p": "2", "cfu": "33.43", "cim": "10.22", "icif": "0.67", "Uzmax": "2340", "Uzh": "2340", "DU": "20", "I0": "20", "fcfu": "33.5", "F0": "75", "a1": "0.0", "Pmax": "990", "Fh": "48", "Ph": "830", "Vh0": "2", "Vh1": "5", "Imax": "377", "abed": "0", "Flat": "Yes", "edep": "1.0", "MotorBlowersSpeed": "-29", "InvNo": "2", "InvCtrCplFlag": "255"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|10|20|30|40|50|60|80|100|120", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "160", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "No", "PowerUpSpeed": "0.3", "PowerDownSpeed": "0.3"}, + "AI": {"Pantstate": "1"}, + "ffList": {"Size": "3", "rows": [ + ["0","2.0"], + ["33.7","2.08"], + ["34.0","2.12"], + ["130.4","8.10"] + ]}, + "ffBrakeList": {"Size": "4", "rows": [ + ["0","2.0"], + ["33.7","2.08"], + ["34.8","2.08"], + ["34.8","2.17"], + ["130.4","8.10"] + ]}, + "LightsList": {"Size": "6", "Wrap": "no", "Default": "1", "rows": [ + ["21","34"], + ["22","34"], + ["16","1"], + ["34","34"], + ["17","34"], + ["38","34"] + ]} +} diff --git a/tests/unit/data/fiz/en57_v1/5bgaiii_led_spot.json b/tests/unit/data/fiz/en57_v1/5bgaiii_led_spot.json new file mode 100644 index 000000000..7ca99dcc3 --- /dev/null +++ b/tests/unit/data/fiz/en57_v1/5bgaiii_led_spot.json @@ -0,0 +1,26 @@ +{ + "_source": "pkp/en57_v1/5bgaiii_led_spot.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "34000", "Vmax": "110", "PWR": "0", "SandCap": "500", "Type": "ezt", "HeatingP": "19.6"}, + "Load": {"MaxLoad": "95", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.286", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "BuffCoupl1": {"ControlType": "en57", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "BuffCoupl2": {"ControlType": "en57", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-887", "PowerFlag": "768"}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4", "BCN": "1", "BCR": "0.203", "BCD": "0.15", "BCM": "9.39", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.013", "TareMaxBP": "2.5", "BVV": "88", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler2"}, + "Cntrl": {"BrakeHandle": "FVel6", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "52", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "3", "SCPN": "3", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "CompartmentLightsStart": "Manual", "BatteryStart": "Disabled", "ConverterStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.315", "CloseSpeed": "0.315", "DoorCloseDelay": "1.6", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"} +} diff --git a/tests/unit/data/fiz/en57_v1/5bgb.json b/tests/unit/data/fiz/en57_v1/5bgb.json new file mode 100644 index 000000000..3075c0a6f --- /dev/null +++ b/tests/unit/data/fiz/en57_v1/5bgb.json @@ -0,0 +1,26 @@ +{ + "_source": "pkp/en57_v1/5bgb.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "34000", "Vmax": "110", "PWR": "0", "SandCap": "500", "Type": "ezt", "HeatingP": "19.6"}, + "Load": {"MaxLoad": "90", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.288", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "BuffCoupl1": {"ControlType": "en57", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-887", "PowerFlag": "768"}, + "BuffCoupl2": {"ControlType": "en57", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4", "BCN": "1", "BCR": "0.203", "BCD": "0.15", "BCM": "9.39", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.013", "TareMaxBP": "2.5", "BVV": "88", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler1"}, + "Cntrl": {"BrakeHandle": "FVel6", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "52", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "3", "SCPN": "3", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "CompartmentLightsStart": "Manual", "ConverterStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.315", "CloseSpeed": "0.315", "DoorCloseDelay": "1.6", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"} +} diff --git a/tests/unit/data/fiz/en57_v1/6ba_840.json b/tests/unit/data/fiz/en57_v1/6ba_840.json new file mode 100644 index 000000000..aed27f1e7 --- /dev/null +++ b/tests/unit/data/fiz/en57_v1/6ba_840.json @@ -0,0 +1,26 @@ +{ + "_source": "pkp/en57_v1/6ba_840.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "34000", "Vmax": "110", "PWR": "0", "SandCap": "500", "Type": "ezt", "HeatingP": "19.6"}, + "Load": {"MaxLoad": "95", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.286", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "BuffCoupl1": {"ControlType": "en57", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "BuffCoupl2": {"ControlType": "en57", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-887", "PowerFlag": "768"}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"}, + "Brake": {"BrakeValve": "EP2", "NBpA": "4", "MBF": "85.0", "MaxBP": "4", "BCN": "1", "BCR": "0.203", "BCD": "0.15", "BCM": "9.39", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.013", "TareMaxBP": "2.5", "BVV": "88", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler2"}, + "Cntrl": {"BrakeHandle": "FVel6", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "52", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "3", "SCPN": "3", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "CompartmentLightsStart": "Manual", "BatteryStart": "Disabled", "ConverterStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.32", "CloseSpeed": "0.242", "DoorCloseDelay": "3.35", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorNeedPermit": "Yes", "DoorAutoCloseVel": "10.0"} +} diff --git a/tests/unit/data/fiz/en57_v1/6bai.json b/tests/unit/data/fiz/en57_v1/6bai.json new file mode 100644 index 000000000..13f1132b7 --- /dev/null +++ b/tests/unit/data/fiz/en57_v1/6bai.json @@ -0,0 +1,25 @@ +{ + "_source": "pkp/en57_v1/6bai.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "34000", "Vmax": "110", "PWR": "0", "SandCap": "500", "Type": "ezt", "HeatingP": "19.6"}, + "Load": {"MaxLoad": "80", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.286", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "K", "NBpA": "4", "MBF": "85.0", "MaxBP": "12", "BCN": "1", "BCR": "0.203", "BCD": "0.15", "BCM": "12.00", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "5.8", "MaxCP": "7.2", "CompressorSpeed": "0.013", "TareMaxBP": "2.5", "BVV": "88", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler2"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.315", "CloseSpeed": "0.315", "DoorCloseDelay": "1.6", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes", "DoorBlocked": "Yes"}, + "BuffCoupl1": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "BuffCoupl2": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-887", "PowerFlag": "768"}, + "Cntrl": {"BrakeHandle": "St113", "BrakeSystem": "ElectroPneumatic", "LocalBrake": "ManualBrake", "MaxBPMass": "52", "BCPN": "5", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "DynamicBrake": "Passive", "MCPN": "3", "SCPN": "3", "IniCDelay": "0.0", "SCDelay": "0.975", "CoupledCtrl": "Yes", "CompartmentLightsStart": "Manual", "BatteryStart": "Disabled", "ConverterStart": "Disabled", "rows": [ + ["-1","0.5","-1","9","Pneumatic"], + ["0","-1","0.0","6","ElectroPneumatic"], + ["1","-1","0.35","4","ElectroPneumatic"], + ["2","-1","-1","0","Pneumatic"], + ["3","-1","-1","3","Pneumatic"], + ["4","0.34","-1","4","Pneumatic"], + ["5","0.0","-1","9","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"} +} diff --git a/tests/unit/data/fiz/en57akl_v1/akl_ra.json b/tests/unit/data/fiz/en57akl_v1/akl_ra.json new file mode 100644 index 000000000..18cc9ed8d --- /dev/null +++ b/tests/unit/data/fiz/en57akl_v1/akl_ra.json @@ -0,0 +1,40 @@ +{ + "_source": "pkp/en57akl_v1/akl_ra.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "38000", "Vmax": "120", "PWR": "0", "SandCap": "500", "Type": "ezt", "HeatingP": "20"}, + "Load": {"MaxLoad": "80", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.286", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.7", "Bd": "14.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.5", "MaxLBP": "3.5", "BCN": "1", "BCR": "0.203", "BCD": "0.105", "BCM": "9.39", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "6.2", "MaxCP": "7.2", "CompressorSpeed": "0.01416667", "TareMaxBP": "2.5", "BVV": "107", "BM": "P10-Bgu", "BCS": "1.5", "BSA": "5", "BRE": "0.9", "CompressorPower": "Coupler2", "UBB1": "2", "UBB2": "8", "LPOn": "3.0", "LPOff": "4.5"}, + "SpringBrake": {"Volume": "10", "MBF": "29", "MaxSP": "7.2", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0001", "ValveOffArea": "0.0001", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.2", "CloseSpeed": "0.2", "DoorCloseDelay": "2.5", "DoorMaxShiftL": "0.58", "DoorMaxShiftR": "0.58", "DoorOpenMethod": "Plug", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.035", "DoorNeedPermit": "Yes", "DoorStayOpen": "5.0", "DoorAutoCloseVel": "10.0"}, + "BuffCoupl1": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39"}, + "BuffCoupl2": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-119", "PowerFlag": "1792"}, + "Cntrl": {"BrakeHandle": "MHZ_EN57", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "52", "BCPN": "10", "BDelay1": "15", "BDelay2": "4", "MainInitTime": "10", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "10", "SCPN": "1", "SpeedCtrlATOF": "1", "EIMCtrlAddZeros": "Yes", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "No", "BrakeOpModes": "PNEPMED", "CompressorStart": "Automatic", "CompartmentLightsStart": "Manual", "ConverterStart": "Disabled", "BatteryStart": "Disabled", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "15", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Blending": {"MED_Vmax": "95", "MED_Vmin": "1", "MED_Vref": "100", "MED_amax": "0.8", "MED_EPVC": "Yes", "MED_Ncor": "No"}, + "SpeedControl": {"SpeedCtrl": "Yes", "OverrideManual": "No", "InitPwr": "1.0", "MaxPwrVel": "-1", "StartVel": "-1", "VelStep": "10", "PwrStep": "0.00", "MinPwr": "1.0", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "120", "Offset": "-0.1", "kPpos": "0.2", "kPneg": "0.4", "kIpos": "0.00", "kIneg": "0.00", "BrakeIntervention": "No", "SpeedCtrlATOF": "1"}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes", "MagnetLocation": "5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Switches": {"Pantograph": "impulse", "Converter": "impulse"}, + "AI": {"Pantstate": "2", "IdlePantUp": "Yes"}, + "Engine": {"EngineType": "ElectricInductionMotor", "Trans": "46:252", "dfic": "377", "dfmax": "0.98", "p": "2", "cfu": "46.8", "cim": "14.2", "icif": "0.671", "Uzmax": "2340", "Uzh": "2340", "DU": "20", "I0": "20", "fcfu": "49", "F0": "130", "a1": "0", "Pmax": "0", "Fh": "130", "Ph": "1000", "Vh0": "5", "Vh1": "10", "Imax": "557", "abed": "0", "edep": "0.95"}, + "LightsList": {"Size": "7", "Wrap": "No", "Default": "1", "rows": [ + ["0","0"], + ["34","34"], + ["38","34"], + ["22","34"], + ["16","1"], + ["1","16"], + ["21","34"] + ]} +} diff --git a/tests/unit/data/fiz/ep05_v2/ep05.json b/tests/unit/data/fiz/ep05_v2/ep05.json new file mode 100644 index 000000000..4340a0b11 --- /dev/null +++ b/tests/unit/data/fiz/ep05_v2/ep05.json @@ -0,0 +1,84 @@ +{ + "_source": "pkp/ep05_v2/ep05.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "82500", "Mred": "12000", "Vmax": "160", "PWR": "2032", "SandCap": "100", "Type": "ep05"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "16.14", "H": "3.987", "W": "2.950", "Cx": "0.2"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "3.05", "Bd": "8.55", "Rmin": "120", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.8", "MaxASBP": "1.3", "RM": "0.56", "BCN": "4", "BCR": "0.1524", "BCD": "0.05", "BCM": "7.13", "Vv": "1.07", "MinCP": "7.5", "MaxCP": "9.0", "BCS": "1.2", "BSA": "0.0", "BM": "P10-Bgu", "BVV": "220", "CompressorSpeed": "0.06", "BRE": "0.85"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103", "ControlType": "EP05"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "ManualBrake": "Yes", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "46", "SCPN": "0", "CoupledCtrl": "Yes", "AutoRelay": "Yes", "SCIM": "1", "IniCDelay": "0.4", "SCDelay": "0.26", "BrakeDelays": "P", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "Camshaft": "Yes", "rows": [ + ["-1","0.60","-1","15","Pneumatic"], + ["0","0.50","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0.00","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "48", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1600", "MinH": "0.38", "MinV": "1900", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "44:77", "WindingRes": "0.126", "nmax": "1270"}, + "Switches": {"PantographPresets": "0|2|3|1"}, + "MotorParamTable0": {"rows": [ + ["0","56.48","80","0.4","370","80","0.445","0"], + ["1","45.15","150","0.3","296","150","0.29","0"], + ["2","41.45","300","0.2","271","300","0.16","0"], + ["3","39.29","400","0.13","257","400","0.13","0"], + ["4","38.36","700","0.04","251","700","0.04","0"], + ["5","33.83","850","0.01","221","850","0.01","0"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "800", "ImaxHi": "800", "IminLo": "350", "IminHi": "450"}, + "RList": {"Size": "46", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.1", "rows": [ + ["0","0","0","0","0","0"], + ["1","100.000","1","4","0","0"], + ["2","14.308","1","4","0","0"], + ["3","10.558","1","4","0","0"], + ["4","8.908","1","4","0","0"], + ["5","7.668","1","4","0","0"], + ["6","6.428","1","4","0","0"], + ["7","5.744","1","4","0","0"], + ["8","5.060","1","4","0","0"], + ["9","4.616","1","4","0","0"], + ["10","4.172","1","4","0","0"], + ["11","3.813","1","4","0","0"], + ["12","3.454","1","4","0","0"], + ["13","3.100","1","4","0","0"], + ["14","2.746","1","4","0","0"], + ["15","2.456","1","4","0","0"], + ["16","2.215","1","4","0","0"], + ["17","1.974","1","4","0","0"], + ["18","1.629","1","4","0","0"], + ["19","1.284","1","4","0","0"], + ["20","1.054","1","4","0","0"], + ["21","0.824","1","4","0","0"], + ["22","0.593","1","4","0","0"], + ["23","0.362","1","4","0","0"], + ["24","0.181","1","4","0","0"], + ["25","0.000","1","4","0","0"], + ["26","0.000","1","4","0","1"], + ["27","0.000","1","4","0","2"], + ["28","0.000","1","4","0","3"], + ["29","0.000","1","4","0","4"], + ["30","0.000","1","4","0","4"], + ["31","0.000","1","4","0","4"], + ["32","0.000","1","4","0","4"], + ["33","2.086","2","2","0","0"], + ["34","1.727","2","2","0","0"], + ["35","1.373","2","2","0","0"], + ["36","1.228","2","2","0","0"], + ["37","0.987","2","2","0","0"], + ["38","0.642","2","2","0","0"], + ["39","0.412","2","2","0","0"], + ["40","0.181","2","2","0","0"], + ["41","0.00","2","2","0","0"], + ["42","0.00","2","2","0","1"], + ["43","0.00","2","2","0","2"], + ["44","0.00","2","2","0","3"], + ["45","0.00","2","2","0","4"], + ["46","0.00","2","2","0","5"] + ]} +} diff --git a/tests/unit/data/fiz/ep09_v2/104e.json b/tests/unit/data/fiz/ep09_v2/104e.json new file mode 100644 index 000000000..9928285cb --- /dev/null +++ b/tests/unit/data/fiz/ep09_v2/104e.json @@ -0,0 +1,76 @@ +{ + "_source": "pkp/ep09_v2/104e.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "10", "UnLoadSpeed": "0.1"}, + "Param": {"Category": "train", "M": "85400", "Mred": "12000", "Vmax": "160", "PWR": "2920", "SandCap": "730"}, + "Dimensions": {"L": "16.74", "H": "4.12", "W": "2.96", "Cx": "0.3"}, + "Wheels": {"D": "1.2", "Tw": "1.435", "AIM": "2000", "Axle": "Bo'Bo'", "Ad": "3.2", "Bd": "7.97", "Rmin": "120", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.6", "MaxASBP": "1.3", "RM": "0.72", "BCN": "8", "BCR": "0.1143", "BCD": "0.05", "BCM": "5.18", "HiPP": "5", "LoPP": "3.5", "Vv": "0.834", "MinCP": "8.5", "MaxCP": "10.0", "CompressorSpeed": "0.058", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "BVV": "278", "CompressorPower": "Main"}, + "SpringBrake": {"Volume": "10", "MBF": "8", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "3.5", "ValveOnArea": "0.0002", "ValveOffArea": "0.0002", "ValvePNBArea": "0.002", "MTC": "4"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "99"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "27", "BDelay2": "4", "BDelay3": "55", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "ManualBrake": "No", "DynamicBrake": "Automatic", "DBAM": "1", "MCPN": "31", "SCPN": "5", "AutoRelay": "No", "SCIM": "255", "IniCDelay": "0.5", "SCDelay": "0.25", "SCDDelay": "0.25", "BrakeDelays": "GPR", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "rows": [ + ["-1","0.7","-1","9","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.44","-1","9","Pneumatic"], + ["2","0.40","-1","9","Pneumatic"], + ["3","0.36","-1","9","Pneumatic"], + ["4","0.32","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0","-1","35","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "3", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "4", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "2000", "MinH": "0.38", "MinV": "1900", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "38:73", "WindingRes": "0.07", "nmax": "1530"}, + "Switches": {"Pantograph": "Continuos", "RelayResetButton1": "1", "RelayResetButton2": "4"}, + "MotorParamTable0": {"rows": [ + ["0","52.81","78","0.53","356.8","78","0.53"], + ["1","42.73","120","0.42","288.7","120","0.42"], + ["2","38.38","193","0.33","259.3","193","0.33"], + ["3","36.60","320","0.24","247.3","320","0.24"], + ["4","36.66","534","0.16","247.7","534","0.16"], + ["5","40.29","1100","0.06","272.2","1100","0.06"] + ]}, + "Circuit": {"CircuitRes": "0.00", "ImaxLo": "850", "ImaxHi": "850", "IminLo": "520", "IminHi": "650"}, + "RList": {"Size": "31", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.1", "rows": [ + ["0","0","0","0","0","0"], + ["1","10.758","1","4","0","2"], + ["2","10.758","1","4","0","0"], + ["3","7.994","1","4","0","0"], + ["4","6.156","1","4","0","0"], + ["5","4.808","1","4","0","0"], + ["6","4.032","1","4","0","0"], + ["7","3.342","1","4","0","0"], + ["8","3.014","1","4","0","0"], + ["9","2.686","1","4","0","0"], + ["10","2.375","1","4","0","0"], + ["11","2.064","1","4","0","0"], + ["12","1.772","1","4","0","0"], + ["13","1.480","1","4","0","0"], + ["14","1.196","1","4","0","0"], + ["15","0.912","1","4","0","0"], + ["16","0.683","1","4","0","0"], + ["17","0.454","1","4","0","0"], + ["18","0.227","1","4","0","0"], + ["19","0","1","4","0","255"], + ["20","5.379","2","2","0","0"], + ["21","3.997","2","2","0","0"], + ["22","3.078","2","2","0","0"], + ["23","2.404","2","2","0","0"], + ["24","2.016","2","2","0","0"], + ["25","1.671","2","2","0","0"], + ["26","1.343","2","2","0","0"], + ["27","1.032","2","2","0","0"], + ["28","0.740","2","2","0","0"], + ["29","0.456","2","2","0","0"], + ["30","0.227","2","2","0","0"], + ["31","0","2","2","0","255"] + ]}, + "WiperList": {"Size": "4", "Angle": "83", "rows": [ + ["0","1.0","0.0","0.5"], + ["3","1.0","5.0","0.5"], + ["3","1.0","0.0","0.5"], + ["3","0.5","0.0","0.5"] + ]} +} diff --git a/tests/unit/data/fiz/et22_v3/201e-rn.json b/tests/unit/data/fiz/et22_v3/201e-rn.json new file mode 100644 index 000000000..05b2e9c8a --- /dev/null +++ b/tests/unit/data/fiz/et22_v3/201e-rn.json @@ -0,0 +1,86 @@ +{ + "_source": "pkp/et22_v3/201e-rn.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "120000", "Mred": "18000", "Vmax": "125", "PWR": "3120", "SandCap": "700", "Type": "et22"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "19.240", "H": "4.055", "W": "3.005", "Cx": "0.3"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "3.05", "Bd": "10.30", "Rmin": "120", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.2", "MaxASBP": "1.8", "BCN": "8", "BCR": "0.1143", "BCD": "0.05", "BCM": "7.75", "HiPP": "5", "LoPP": "3.5", "Vv": "1", "MinCP": "7.5", "MaxCP": "8.5", "CompressorSpeed": "0.0566", "BVV": "250", "RM": "0.677", "BSA": "0", "BCS": "1", "BM": "P10-Bgu", "CompressorPower": "Main", "BRE": "0.75"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "99"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "17", "BDelay2": "7", "BDelay3": "45", "BDelay4": "25", "BrakeDelays": "GPR", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "ManualBrake": "Yes", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "48", "SCPN": "6", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.26", "SCDDelay": "0.05", "MotorBlowersStart": "Converter", "MotorBlowersSpeed": "-1310", "rows": [ + ["-1","0.6","-1","12","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.43","-1","9","Pneumatic"], + ["2","0.40","-1","9","Pneumatic"], + ["3","0.37","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0","-1","18","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1800", "MinH": "0.38", "MinV": "1900", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "18:79", "WindingRes": "0.14", "nmax": "2390"}, + "MotorParamTable0": {"rows": [ + ["0","21.4","105","0.12","141.5","105","0.12"], + ["1","19.57","140","0.07","129.3","140","0.07"], + ["2","20.42","190","0.09","134.7","190","0.09"], + ["3","22.97","160","0.25","152.1","160","0.25"], + ["4","22.12","255","0.16","145.8","255","0.16"], + ["5","24.18","170","0.35","158.2","170","0.35"], + ["6","22.76","800","0.02","149","800","0.02"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "600", "ImaxHi": "750", "IminLo": "350", "IminHi": "450"}, + "RList": {"Size": "48", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.2", "rows": [ + ["0","0","0","0","0"], + ["1","25.88","1","6","0"], + ["2","15.05","1","6","0"], + ["3","11.02","1","6","0"], + ["4","8.98","1","6","0"], + ["5","7.54","1","6","0"], + ["6","6.28","1","6","0"], + ["7","5.50","1","6","0"], + ["8","5.11","1","6","0"], + ["9","4.67","1","6","0"], + ["10","4.11","1","6","0"], + ["11","3.61","1","6","0"], + ["12","3.06","1","6","0"], + ["13","2.70","1","6","0"], + ["14","2.38","1","6","0"], + ["15","2.05","1","6","0"], + ["16","1.69","1","6","0"], + ["17","1.36","1","6","0"], + ["18","1.00","1","6","0"], + ["19","0.84","1","6","0"], + ["20","0.29","1","6","0"], + ["21","0.00","1","6","0"], + ["22","5.53","2","3","0"], + ["23","4.53","2","3","0"], + ["24","3.58","2","3","0"], + ["25","3.03","2","3","0"], + ["26","2.60","2","3","0"], + ["27","2.26","2","3","0"], + ["28","1.97","2","3","0"], + ["29","1.60","2","3","0"], + ["30","1.46","2","3","0"], + ["31","1.26","2","3","0"], + ["32","1.00","2","3","0"], + ["33","0.61","2","3","0"], + ["34","0.35","2","3","0"], + ["35","0.23","2","3","0"], + ["36","0.00","2","3","0"], + ["37","2.64","3","2","0"], + ["38","2.23","3","2","0"], + ["39","1.78","3","2","0"], + ["40","1.49","3","2","0"], + ["41","1.29","3","2","0"], + ["42","1.11","3","2","0"], + ["43","0.95","3","2","0"], + ["44","0.78","3","2","0"], + ["45","0.62","3","2","0"], + ["46","0.47","3","2","0"], + ["47","0.22","3","2","0"], + ["48","0.00","3","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/et22_v3/201e-w.json b/tests/unit/data/fiz/et22_v3/201e-w.json new file mode 100644 index 000000000..80b9aedda --- /dev/null +++ b/tests/unit/data/fiz/et22_v3/201e-w.json @@ -0,0 +1,86 @@ +{ + "_source": "pkp/et22_v3/201e-w.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "120000", "Mred": "18000", "Vmax": "125", "PWR": "3120", "SandCap": "700", "Type": "et22"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "19.240", "H": "4.055", "W": "3.005", "Cx": "0.3"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "3.05", "Bd": "10.30", "Rmin": "120", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.2", "MaxASBP": "1.8", "BCN": "8", "BCR": "0.1143", "BCD": "0.05", "BCM": "7.75", "HiPP": "5", "LoPP": "3.5", "Vv": "1", "MinCP": "7.5", "MaxCP": "8.5", "CompressorSpeed": "0.0566", "BVV": "250", "RM": "0.677", "BSA": "0", "BCS": "1", "BM": "P10-Bgu", "CompressorPower": "Main", "BRE": "0.75"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "99"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "17", "BDelay2": "7", "BDelay3": "45", "BDelay4": "25", "BrakeDelays": "GPR", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "ManualBrake": "Yes", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "48", "SCPN": "6", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.26", "SCDDelay": "0.05", "MotorBlowersStart": "Converter", "MotorBlowersSpeed": "-1310", "rows": [ + ["-1","0.6","-1","12","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.43","-1","9","Pneumatic"], + ["2","0.40","-1","9","Pneumatic"], + ["3","0.37","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0","-1","18","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1800", "MinH": "0.38", "MinV": "1900", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "18:79", "WindingRes": "0.14", "nmax": "2390"}, + "MotorParamTable0": {"rows": [ + ["0","21.4","105","0.12","141.5","105","0.12"], + ["1","19.57","140","0.07","129.3","140","0.07"], + ["2","20.42","190","0.09","134.7","190","0.09"], + ["3","22.97","160","0.25","152.1","160","0.25"], + ["4","22.12","255","0.16","145.8","255","0.16"], + ["5","24.18","170","0.35","158.2","170","0.35"], + ["6","22.76","800","0.02","149","800","0.02"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "600", "ImaxHi": "750", "IminLo": "350", "IminHi": "450"}, + "RList": {"Size": "48", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.2", "rows": [ + ["0","0","0","0","0"], + ["1","25.88","1","6","0"], + ["2","15.05","1","6","0"], + ["3","11.02","1","6","0"], + ["4","8.98","1","6","0"], + ["5","7.54","1","6","0"], + ["6","6.28","1","6","0"], + ["7","5.50","1","6","0"], + ["8","5.11","1","6","0"], + ["9","4.67","1","6","0"], + ["10","4.11","1","6","0"], + ["11","3.61","1","6","0"], + ["12","3.06","1","6","0"], + ["13","2.70","1","6","0"], + ["14","2.38","1","6","0"], + ["15","2.05","1","6","0"], + ["16","1.69","1","6","0"], + ["17","1.36","1","6","0"], + ["18","1.00","1","6","0"], + ["19","0.84","1","6","0"], + ["20","0.29","1","6","0"], + ["21","0.00","1","6","0"], + ["22","5.53","2","3","0"], + ["23","4.53","2","3","0"], + ["24","3.58","2","3","0"], + ["25","3.03","2","3","0"], + ["26","2.60","2","3","0"], + ["27","2.26","2","3","0"], + ["28","1.97","2","3","0"], + ["29","1.60","2","3","0"], + ["30","1.46","2","3","0"], + ["31","1.26","2","3","0"], + ["32","1.00","2","3","0"], + ["33","0.61","2","3","0"], + ["34","0.35","2","3","0"], + ["35","0.23","2","3","0"], + ["36","0.00","2","3","0"], + ["37","2.64","3","2","0"], + ["38","2.23","3","2","0"], + ["39","1.78","3","2","0"], + ["40","1.49","3","2","0"], + ["41","1.29","3","2","0"], + ["42","1.11","3","2","0"], + ["43","0.95","3","2","0"], + ["44","0.78","3","2","0"], + ["45","0.62","3","2","0"], + ["46","0.47","3","2","0"], + ["47","0.22","3","2","0"], + ["48","0.00","3","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/et42_v2/112e-a.json b/tests/unit/data/fiz/et42_v2/112e-a.json new file mode 100644 index 000000000..f1ed904ff --- /dev/null +++ b/tests/unit/data/fiz/et42_v2/112e-a.json @@ -0,0 +1,77 @@ +{ + "_source": "pkp/et42_v2/112e-a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "82000", "Vmax": "100", "PWR": "2240", "SandCap": "850", "Type": "et42"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "15.43", "H": "4.62", "W": "2.95", "Cx": "0.3"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "3.05", "Bd": "8.55", "Rmin": "125", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.0", "MaxASBP": "1.5", "BCN": "4", "BCR": "0.127", "BCD": "0.12", "BCM": "5.76", "Vv": "1.2", "MinCP": "7.5", "MaxCP": "9.0", "BCS": "1", "BSA": "0.5", "BM": "P10-Bg", "BVV": "300", "CompressorSpeed": "0.0392", "CompressorPower": "Main", "BRE": "0.7"}, + "BuffCoupl1": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "99"}, + "BuffCoupl2": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BrakeDelays": "GP", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", "DynamicBrake": "Switch", "MCPN": "38", "SCPN": "5", "AutoRelay": "No", "SCIM": "255", "IniCDelay": "0.5", "SCDelay": "0.1", "rows": [ + ["-1","0.60","-1","15","Pneumatic"], + ["0","0.50","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0.00","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "3600", "MaxCurrent": "1300", "MinH": "0.42", "MinV": "1900", "MaxH": "1.77", "CSW": "1.4", "PhysicalLayout": "1"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "25:86", "WindingRes": "0.5", "nmax": "1500"}, + "Switches": {"Pantograph": "Continuos", "MotorConnectors": "Toggle"}, + "MotorParamTable": {"rows": [ + ["0","21.86","205","142","150"], + ["1","21.49","244","139","187"], + ["2","21.48","299","138","234"], + ["3","19.77","320","136","297"], + ["4","23.75","581","126","333"], + ["5","24.47","768","123","412"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "650", "ImaxHi": "651", "IminLo": "415", "IminHi": "415"}, + "RList": {"Size": "38", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.2", "rows": [ + ["0","0","0","0","0","0"], + ["1","10.566","1","4","0","5"], + ["2","10.566","1","4","0","3"], + ["3","10.566","1","4","0","0"], + ["4","9.136","1","4","0","0"], + ["5","7.706","1","4","0","0"], + ["6","6.881","1","4","0","0"], + ["7","6.056","1","4","0","0"], + ["8","5.682","1","4","0","0"], + ["9","5.308","1","4","0","0"], + ["10","4.932","1","4","0","0"], + ["11","4.556","1","4","0","0"], + ["12","4.211","1","4","0","0"], + ["13","3.866","1","4","0","0"], + ["14","3.538","1","4","0","0"], + ["15","3.21","1","4","0","0"], + ["16","2.899","1","4","0","0"], + ["17","2.588","1","4","0","0"], + ["18","2.296","1","4","0","0"], + ["19","2.004","1","4","0","0"], + ["20","1.72","1","4","0","0"], + ["21","1.436","1","4","0","0"], + ["22","1.055","1","4","0","0"], + ["23","0.674","1","4","0","0"], + ["24","0.337","1","4","0","0"], + ["25","0.00","1","4","0","255"], + ["26","5.283","2","2","0","0"], + ["27","4.568","2","2","0","0"], + ["28","3.853","2","2","0","0"], + ["29","3.028","2","2","0","0"], + ["30","2.654","2","2","0","0"], + ["31","2.278","2","2","0","0"], + ["32","1.933","2","2","0","0"], + ["33","1.605","2","2","0","0"], + ["34","1.294","2","2","0","0"], + ["35","1.002","2","2","0","0"], + ["36","0.718","2","2","0","0"], + ["37","0.337","2","2","0","0"], + ["38","0.0","2","2","0","255"] + ]} +} diff --git a/tests/unit/data/fiz/et42_v2/112e-b.json b/tests/unit/data/fiz/et42_v2/112e-b.json new file mode 100644 index 000000000..4c6ec9ede --- /dev/null +++ b/tests/unit/data/fiz/et42_v2/112e-b.json @@ -0,0 +1,77 @@ +{ + "_source": "pkp/et42_v2/112e-b.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "82000", "Vmax": "100", "PWR": "2240", "SandCap": "850", "Type": "et42"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "15.43", "H": "4.62", "W": "2.95", "Cx": "0.3"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "3.05", "Bd": "8.55", "Rmin": "125", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.0", "MaxASBP": "1.5", "BCN": "4", "BCR": "0.127", "BCD": "0.12", "BCM": "5.76", "Vv": "1.2", "MinCP": "7.5", "MaxCP": "9.0", "BCS": "1", "BSA": "0.5", "BM": "P10-Bg", "BVV": "300", "CompressorSpeed": "0.0392", "CompressorPower": "Main", "BRE": "0.7"}, + "BuffCoupl1": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "BuffCoupl2": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "99"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BrakeDelays": "GP", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", "DynamicBrake": "Switch", "MCPN": "38", "SCPN": "5", "AutoRelay": "No", "SCIM": "255", "IniCDelay": "0.5", "SCDelay": "0.1", "rows": [ + ["-1","0.60","-1","15","Pneumatic"], + ["0","0.50","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0.00","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "3600", "MaxCurrent": "1300", "MinH": "0.42", "MinV": "1900", "MaxH": "1.77", "CSW": "1.4", "PhysicalLayout": "1"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "25:86", "WindingRes": "0.5", "nmax": "1500"}, + "Switches": {"Pantograph": "Continuos", "MotorConnectors": "Toggle"}, + "MotorParamTable": {"rows": [ + ["0","21.86","205","142","150"], + ["1","21.49","244","139","187"], + ["2","21.48","299","138","234"], + ["3","19.77","320","136","297"], + ["4","23.75","581","126","333"], + ["5","24.47","768","123","412"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "650", "ImaxHi": "651", "IminLo": "415", "IminHi": "415"}, + "RList": {"Size": "38", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "0.2", "rows": [ + ["0","0","0","0","0","0"], + ["1","10.566","1","4","0","5"], + ["2","10.566","1","4","0","3"], + ["3","10.566","1","4","0","0"], + ["4","9.136","1","4","0","0"], + ["5","7.706","1","4","0","0"], + ["6","6.881","1","4","0","0"], + ["7","6.056","1","4","0","0"], + ["8","5.682","1","4","0","0"], + ["9","5.308","1","4","0","0"], + ["10","4.932","1","4","0","0"], + ["11","4.556","1","4","0","0"], + ["12","4.211","1","4","0","0"], + ["13","3.866","1","4","0","0"], + ["14","3.538","1","4","0","0"], + ["15","3.21","1","4","0","0"], + ["16","2.899","1","4","0","0"], + ["17","2.588","1","4","0","0"], + ["18","2.296","1","4","0","0"], + ["19","2.004","1","4","0","0"], + ["20","1.72","1","4","0","0"], + ["21","1.436","1","4","0","0"], + ["22","1.055","1","4","0","0"], + ["23","0.674","1","4","0","0"], + ["24","0.337","1","4","0","0"], + ["25","0.00","1","4","0","255"], + ["26","5.283","2","2","0","0"], + ["27","4.568","2","2","0","0"], + ["28","3.853","2","2","0","0"], + ["29","3.028","2","2","0","0"], + ["30","2.654","2","2","0","0"], + ["31","2.278","2","2","0","0"], + ["32","1.933","2","2","0","0"], + ["33","1.605","2","2","0","0"], + ["34","1.294","2","2","0","0"], + ["35","1.002","2","2","0","0"], + ["36","0.718","2","2","0","0"], + ["37","0.337","2","2","0","0"], + ["38","0.0","2","2","0","255"] + ]} +} diff --git a/tests/unit/data/fiz/eu04_v1/eu04-01.json b/tests/unit/data/fiz/eu04_v1/eu04-01.json new file mode 100644 index 000000000..bf5354005 --- /dev/null +++ b/tests/unit/data/fiz/eu04_v1/eu04-01.json @@ -0,0 +1,67 @@ +{ + "_source": "pkp/eu04_v1/eu04-01.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "87200", "Mred": "12000", "Vmax": "110", "PWR": "1680", "SandCap": "500", "type": "EU04"}, + "Dimensions": {"L": "16.238", "H": "4.343", "W": "2.903", "Cx": "0.3"}, + "Wheels": {"D": "1.350", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "3.05", "Bd": "8.55", "Rmin": "140", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "K", "NBpA": "4", "MBF": "85.0", "MaxBP": "2.8", "RM": "0.43", "LocMaxBP": "3.5", "BCN": "8", "BCR": "0.1524", "BCD": "0.12", "BCM": "8.7", "HiPP": "5", "LoPP": "3.5", "Vv": "0.83", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.03", "BCS": "0.8", "BSA": "0", "BM": "P10-Bgu", "BVV": "200", "BRE": "0.8"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103"}, + "Cntrl": {"BrakeDelays": "GPRR", "BrakeHandle": "Knorr", "LocBrakeHandle": "Knorr", "BrakeSystem": "Pneumatic", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ManualBrake": "Yes", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "31", "SCPN": "8", "AutoRelay": "Optional", "IniCDelay": "0.4", "SCDelay": "0.4", "rows": [ + ["-1","0.90","0","15","Pneumatic"], + ["0","0.50","0","9","Pneumatic"], + ["1","-1","-1","0","Pneumatic"], + ["2","-1","-1","5","Pneumatic"], + ["3","0.001","-1","9","Pneumatic"], + ["4","0.00","-1","15","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1000", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "3"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "21:81", "WindingRes": "0.1411", "nmax": "2075"}, + "MotorParamTable0": {"rows": [ + ["0","23.9","200","-0.08","155.9","200","-0.08","1"], + ["1","23.28","200","-0.07","151.8","200","-0.07","1"], + ["2","22.45","200","-0.06","146.5","200","-0.06","1"], + ["3","22.18","200","0","144.7","200","0","1"], + ["4","23.09","300","-0.05","150.6","300","-0.05","1"], + ["5","22.89","290","-0.02","149.3","290","-0.02","1"], + ["6","23.59","360","-0.02","153.9","360","-0.02","1"], + ["7","23.95","410","0.01","156.2","410","0.01","1"], + ["8","21.94","410","0.05","143.1","410","0.05","0"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "650", "ImaxHi": "650", "IminLo": "350", "IminHi": "450"}, + "RList": {"Size": "31", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "50", "rows": [ + ["0","0","0","0","0"], + ["1","16.9","1","4","1"], + ["2","12.4","1","4","1"], + ["3","9.77","1","4","1"], + ["4","8.0","1","4","1"], + ["5","6.4","1","4","1"], + ["6","4.94","1","4","1"], + ["7","4.38","1","4","1"], + ["8","3.9","1","4","1"], + ["9","3.37","1","4","1"], + ["10","2.86","1","4","1"], + ["11","2.5","1","4","1"], + ["12","2.1","1","4","1"], + ["13","1.78","1","4","1"], + ["14","1.49","1","4","1"], + ["15","1.18","1","4","1"], + ["16","0.89","1","4","1"], + ["17","0.66","1","4","1"], + ["18","0.42","1","4","1"], + ["19","0.2","1","4","1"], + ["20","0.0","1","4","1"], + ["21","6.09","2","2","1"], + ["22","3.9","2","2","1"], + ["23","3.1","2","2","1"], + ["24","2.57","2","2","1"], + ["25","2.185","2","2","1"], + ["26","1.58","2","2","1"], + ["27","1.115","2","2","1"], + ["28","0.795","2","2","1"], + ["29","0.5","2","2","1"], + ["30","0.23","2","2","1"], + ["31","0.0","2","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/ew58_v1/ew58-d.json b/tests/unit/data/fiz/ew58_v1/ew58-d.json new file mode 100644 index 000000000..67a381d36 --- /dev/null +++ b/tests/unit/data/fiz/ew58_v1/ew58-d.json @@ -0,0 +1,20 @@ +{ + "_source": "pkp/ew58_v1/ew58-d.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "42200", "Mred": "1800", "Vmax": "120", "PWR": "2", "SandCap": "0", "Type": "ezt", "HeatingP": "18"}, + "Load": {"MaxLoad": "220", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "6", "UnLoadSpeed": "6"}, + "Dimensions": {"L": "21.7", "H": "4.12", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.92", "AIM": "2000", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.75", "Bd": "15.6", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP1", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.8", "BCN": "4", "BCR": "0.1143", "BCD": "0.08", "BCM": "12.156", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.013", "TareMaxBP": "2.4", "BVV": "150", "BM": "P10-Bg", "BCS": "1.5", "BSA": "16.00", "BRE": "0.83"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.35", "CloseSpeed": "0.35", "DoorCloseDelay": "0.5", "DoorMaxShiftL": "0.53", "DoorMaxShiftR": "0.53", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes"}, + "BuffCoupl": {"ControlType": "ew58", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "MaxBPMass": "68", "BCPN": "1", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "DynamicBrake": "Passive", "MCPN": "4", "SCPN": "9", "SCIM": "1", "AutoRelay": "Yes", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "No", "BrakeDelays": "R", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","-1","-1","0","ElectroPneumatic"], + ["1","-1","0.35","4","ElectroPneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "MinPress": "3.5", "PhysicalLayout": "3"}, + "DCEMUED": {"CouplerCheck": "3", "EP_max_Vel": "48"}, + "Switches": {"Pantograph": "Continous"} +} diff --git a/tests/unit/data/fiz/ew58_v1/ew58-sa.json b/tests/unit/data/fiz/ew58_v1/ew58-sa.json new file mode 100644 index 000000000..95cc27446 --- /dev/null +++ b/tests/unit/data/fiz/ew58_v1/ew58-sa.json @@ -0,0 +1,68 @@ +{ + "_source": "pkp/ew58_v1/ew58-sa.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "52400", "Mred": "4980", "Vmax": "120", "PWR": "820", "SandCap": "500", "Type": "ezt", "HeatingP": "21"}, + "Load": {"MaxLoad": "220", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "6", "UnLoadSpeed": "6"}, + "Dimensions": {"L": "21.55", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "1.0", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP1", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.8", "BCN": "4", "BCR": "0.1143", "BCD": "0.08", "BCM": "12.156", "HiPP": "5", "LoPP": "3.5", "TareMaxBP": "2.9", "BVV": "150", "BM": "P10-Bg", "BCS": "1.5", "BSA": "16.00", "BRE": "0.83"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.35", "CloseSpeed": "0.35", "DoorCloseDelay": "0.5", "DoorMaxShiftL": "0.53", "DoorMaxShiftR": "0.53", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes"}, + "BuffCoupl1": {"ControlType": "ew58", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39"}, + "BuffCoupl2": {"ControlType": "ew58", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "Cntrl": {"BrakeHandle": "FVE408", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "68", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "4", "SCPN": "9", "DBAM": "2", "IniCDelay": "0.0", "SCDelay": "0.3", "CoupledCtrl": "No", "SCIM": "1", "AutoRelay": "Yes", "DynamicBrake": "Automatic", "ConverterStart": "Disabled", "BatteryStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "0", "MaxVoltage": "3750", "MaxCurrent": "800"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "750", "Trans": "18:69", "WindingRes": "0.1097", "nmax": "2390"}, + "Blending": {"MED_amax": "1.00"}, + "DCEMUED": {"CouplerCheck": "2", "EP_min_Im": "80"}, + "Switches": {"Pantograph": "Continous", "RelayResetButton1": "101"}, + "MotorParamTable0": {"rows": [ + ["0","11.3","90","0.21","79.5","90","0.21","1"], + ["1","12.39","110","0.24","83.9","110","0.24","1"], + ["2","11.88","123","0.23","80.5","123","0.23","1"], + ["3","11.37","135","0.22","77.2","135","0.22","1"], + ["4","11.05","180","0.2","74.6","180","0.2","1"], + ["5","10.73","225","0.18","72.1","225","0.18","1"], + ["6","10.26","250","0.18","68.7","250","0.18","1"], + ["7","9.8","275","0.19","65.4","275","0.19","1"], + ["8","9.34","300","0.19","62","300","0.19","1"], + ["9","8.15","455","0.1","55.5","455","0.1","0"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "580", "ImaxHi": "581", "IminLo": "360", "IminHi": "455", "TUHEX_Sum1": "365", "TUHEX_Sum2": "500", "TUHEX_Sum3": "610", "TUHEX_Diff": "10", "TUHEX_MaxIw": "280", "TUHEX_MinIw": "11", "TUHEX_Stages": "3"}, + "RList": {"Size": "22", "RVent": "No", "DynBrakeRes1": "4.3", "DynBrakeRes2": "1.956", "rows": [ + ["0","0","0","0","0","0"], + ["1","17.001","1","4","0","0"], + ["2","10.677","1","4","1","0"], + ["2","7.573","1","4","1","0"], + ["2","5.505","1","4","1","0"], + ["2","4.477","1","4","1","0"], + ["2","3.589","1","4","1","0"], + ["2","2.821","1","4","1","0"], + ["2","2.157","1","4","1","0"], + ["2","1.585","1","4","1","0"], + ["2","1.116","1","4","1","0"], + ["2","0.688","1","4","1","0"], + ["2","0.320","1","4","1","0"], + ["2","0.000","1","4","1","0"], + ["3","0.000","1","4","1","1"], + ["3","0.000","1","4","1","2"], + ["3","0.000","1","4","1","3"], + ["3","0.000","1","4","1","4"], + ["3","0.000","1","4","1","5"], + ["4","0.000","1","4","1","6"], + ["4","0.000","1","4","1","7"], + ["4","0.000","1","4","1","8"], + ["4","0.000","1","4","1","9"] + ]} +} diff --git a/tests/unit/data/fiz/ew58_v1/ew58-sb.json b/tests/unit/data/fiz/ew58_v1/ew58-sb.json new file mode 100644 index 000000000..f08052daa --- /dev/null +++ b/tests/unit/data/fiz/ew58_v1/ew58-sb.json @@ -0,0 +1,68 @@ +{ + "_source": "pkp/ew58_v1/ew58-sb.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "52400", "Mred": "4980", "Vmax": "120", "PWR": "820", "SandCap": "500", "Type": "ezt", "HeatingP": "21"}, + "Load": {"MaxLoad": "220", "LoadQ": "pieces", "LoadAccepted": "Passengers,Pantstate", "OverLoadFactor": "2", "LoadSpeed": "6", "UnLoadSpeed": "6"}, + "Dimensions": {"L": "21.55", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "1.0", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EP1", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.8", "BCN": "4", "BCR": "0.1143", "BCD": "0.08", "BCM": "12.156", "HiPP": "5", "LoPP": "3.5", "TareMaxBP": "2.5", "BVV": "150", "BM": "P10-Bg", "BCS": "1.5", "BSA": "16.00", "BRE": "0.83"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.35", "CloseSpeed": "0.35", "DoorCloseDelay": "0.5", "DoorMaxShiftL": "0.53", "DoorMaxShiftR": "0.53", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes"}, + "BuffCoupl1": {"CControlType": "ew58", "Type": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "-127"}, + "BuffCoupl2": {"ControlType": "ew58", "CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39"}, + "Cntrl": {"BrakeHandle": "FVE408", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "68", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "ManualBrake", "MCPN": "4", "SCPN": "9", "DBAM": "2", "IniCDelay": "0.0", "SCDelay": "0.3", "CoupledCtrl": "No", "SCIM": "1", "AutoRelay": "Yes", "DynamicBrake": "Automatic", "ConverterStart": "Disabled", "BatteryStart": "Disabled", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Clima": {"Heating": "Main"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "0", "MaxVoltage": "3750", "MaxCurrent": "800"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "750", "Trans": "18:69", "WindingRes": "0.1097", "nmax": "2390"}, + "Blending": {"MED_amax": "1.00"}, + "DCEMUED": {"CouplerCheck": "1", "EP_min_Im": "80"}, + "Switches": {"Pantograph": "Continous", "RelayResetButton1": "101"}, + "MotorParamTable0": {"rows": [ + ["0","11.3","90","0.21","79.5","90","0.21","1"], + ["1","12.39","110","0.24","83.9","110","0.24","1"], + ["2","11.88","123","0.23","80.5","123","0.23","1"], + ["3","11.37","135","0.22","77.2","135","0.22","1"], + ["4","11.05","180","0.2","74.6","180","0.2","1"], + ["5","10.73","225","0.18","72.1","225","0.18","1"], + ["6","10.26","250","0.18","68.7","250","0.18","1"], + ["7","9.8","275","0.19","65.4","275","0.19","1"], + ["8","9.34","300","0.19","62","300","0.19","1"], + ["9","8.15","455","0.1","55.5","455","0.1","0"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "580", "ImaxHi": "581", "IminLo": "360", "IminHi": "455", "TUHEX_Sum1": "365", "TUHEX_Sum2": "500", "TUHEX_Sum3": "610", "TUHEX_Diff": "10", "TUHEX_MaxIw": "280", "TUHEX_MinIw": "11", "TUHEX_Stages": "3"}, + "RList": {"Size": "22", "RVent": "No", "DynBrakeRes1": "4.3", "DynBrakeRes2": "1.956", "rows": [ + ["0","0","0","0","0","0"], + ["1","17.001","1","4","0","0"], + ["2","10.677","1","4","1","0"], + ["2","7.573","1","4","1","0"], + ["2","5.505","1","4","1","0"], + ["2","4.477","1","4","1","0"], + ["2","3.589","1","4","1","0"], + ["2","2.821","1","4","1","0"], + ["2","2.157","1","4","1","0"], + ["2","1.585","1","4","1","0"], + ["2","1.116","1","4","1","0"], + ["2","0.688","1","4","1","0"], + ["2","0.320","1","4","1","0"], + ["2","0.000","1","4","1","0"], + ["3","0.000","1","4","1","1"], + ["3","0.000","1","4","1","2"], + ["3","0.000","1","4","1","3"], + ["3","0.000","1","4","1","4"], + ["3","0.000","1","4","1","5"], + ["4","0.000","1","4","1","6"], + ["4","0.000","1","4","1","7"], + ["4","0.000","1","4","1","8"], + ["4","0.000","1","4","1","9"] + ]} +} diff --git a/tests/unit/data/fiz/impuls_v1/31we-a_pr.json b/tests/unit/data/fiz/impuls_v1/31we-a_pr.json new file mode 100644 index 000000000..368c0f24f --- /dev/null +++ b/tests/unit/data/fiz/impuls_v1/31we-a_pr.json @@ -0,0 +1,50 @@ +{ + "_source": "pkp/impuls_v1/31we-a_pr.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "31800", "Vmax": "160", "PWR": "1000", "SandCap": "200", "Type": "ezt"}, + "Load": {"MaxLoad": "42", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "1"}, + "Dimensions": {"L": "21.06", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.840", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.37", "MaxLBP": "4.37", "BCN": "4", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.125", "MinCP": "7.5", "MaxCP": "9.0", "MinCP_B": "8.5", "MaxCP_B": "10.0", "CompressorSpeed": "0.00625", "BVV": "125", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "3.85", "CompressorPower": "Converter", "LPOn": "2.8", "LPOff": "4.3", "UBB1": "2", "EmergencyCutsOffHandle": "Yes"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.21", "CloseSpeed": "0.19", "DoorCloseDelay": "2.1", "DoorOpenWithPermit": "1", "DoorMaxShiftL": "0.7", "DoorMaxShiftR": "0.7", "DoorOpenMethod": "Plug", "DoorMaxShiftPlug": "0.028", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorAutoCloseRemote": "Yes", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes", "MirrorVelClose": "20", "DoorsPermitLightBlinking": "2"}, + "BuffCoupl1": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39", "ControlType": "31we_pr"}, + "BuffCoupl2": {"CType": "Articulated", "AllowedFlag": "-255", "ControlType": "impuls"}, + "Cntrl": {"BrakeHandle": "MHZ_6P", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "37.8", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "MCPN": "10", "SCPN": "1", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "No", "EIMCtrlAddZeros": "Yes", "SBD": "0.65", "ASB": "Yes", "ConverterStart": "Automatic", "ConverterStartDelay": "8", "MainInitTime": "2", "BrakeOpModes": "PN", "MotorBlowersStart": "Converter", "EIMCtrlEmergency": "Yes", "PantEPValveStart": "Automatic", "PantEPValveSpring": "No", "SpringBrakeCutsOffDrive": "No", "SpringBrakeDriveEmergencyVel": "2", "ReleaseParkingBySpringBrakeWhenDoorIsOpen": "Yes", "CompartmentLightsStart": "Manual", "HAO": "Yes", "OMP": "0.8", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "239", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes", "MagnetLocation": "4.5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "300"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "0", "MaxVoltage": "3900", "MinV": "2100", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4"}, + "Engine": {"EngineType": "ElectricInductionMotor", "InvNo": "2", "Trans": "60:252", "dfic": "794", "dfmax": "1.500", "p": "2", "cfu": "40.1", "cim": "12.24", "icif": "0.679", "Uzmax": "2200", "Uzh": "1500", "DU": "20", "I0": "10", "fcfu": "47.6", "fcfuh": "31.4", "F0": "100", "a1": "0", "Pmax": "1260", "Fh": "48", "Ph": "800", "Vh0": "5", "Vh1": "15", "Imax": "475", "abed": "0", "edep": "3.0", "eimclf": "Yes", "MotorBlowersSpeed": "-15", "MotorBlowersSustainTime": "300", "MotorBlowersStartVelocity": "0.1"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "10|20|40|60|80|100|120|140|160", "OverrideManual": "No", "InitPwr": "1.0", "MaxPwrVel": "-1", "StartVel": "-1", "VelStep": "10", "PwrStep": "0.0", "MinPwr": "0.2", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "160", "Offset": "-0.5", "kPpos": "0.2", "kPneg": "0.4", "kIpos": "0.00", "kIneg": "0.00", "BrakeIntervention": "No", "SpeedCtrlATOF": "2"}, + "AI": {"Pantstate": "2", "LocalBrakeAccFactor": "0.5", "IdlePantUp": "No"}, + "Blending": {"MED_amax": "1.08", "MED_EPVC": "No", "MED_Ncor": "No", "MED_FrEDFactor": "0", "MED_FirstDelayED": "0.5", "MED_ScndDelayED": "0.25"}, + "Switches": {"PantographPresets": "0|1|2|3"}, + "Circuit": {"ImaxHi": "500"}, + "LightsList": {"Size": "8", "Wrap": "No", "Default": "1", "rows": [ + ["0","0"], + ["388","34"], + ["21","34"], + ["1","16"], + ["16","1"], + ["22","34"], + ["38","38"], + ["34","34"] + ]}, + "ffList": {"Size": "5", "rows": [ + ["0","0.5"], + ["24","1.0"], + ["35","1.0"], + ["42","1.3"], + ["42","1.3"], + ["74","2.3"] + ]} +} diff --git a/tests/unit/data/fiz/impuls_v1/31we-b_pr.json b/tests/unit/data/fiz/impuls_v1/31we-b_pr.json new file mode 100644 index 000000000..60138291f --- /dev/null +++ b/tests/unit/data/fiz/impuls_v1/31we-b_pr.json @@ -0,0 +1,20 @@ +{ + "_source": "pkp/impuls_v1/31we-b_pr.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "38700", "Vmax": "160", "PWR": "2", "SandCap": "0", "Type": "ezt"}, + "Load": {"MaxLoad": "48", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "15.92", "H": "4.12", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.84", "AIM": "500", "Tw": "1.435", "Axle": "2'1'", "Ad": "2.75", "Bd": "15.92", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "2", "MBF": "85.0", "MaxBP": "4.06", "MaxLBP": "4.06", "BCN": "6", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.01", "MinCP": "10.0", "MaxCP": "12.0", "CompressorSpeed": "0.0000001", "BVV": "187", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "3.09", "CompressorPower": "Converter"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.8", "PressOn": "4.5", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.21", "CloseSpeed": "0.19", "DoorCloseDelay": "2.1", "DoorOpenWithPermit": "1", "DoorMaxShiftL": "0.7", "DoorMaxShiftR": "0.7", "DoorOpenMethod": "Plug", "DoorMaxShiftPlug": "0.028", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorAutoCloseRemote": "Yes", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes"}, + "BuffCoupl": {"CType": "Articulated", "AllowedFlag": "-255", "ControlType": "impuls"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocBrakeHandle": "FD1", "MaxBPMass": "53", "BCPN": "2", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "10", "SCPN": "0", "AutoRelay": "Yes", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "Yes", "ASB": "Yes", "CompartmentLightsStart": "Manual", "HAO": "Yes", "OMP": "0.8", "PantEPValveStart": "Automatic", "PantEPValveSpring": "No", "PantCompressorStart": "Automatic", "PantAutoValve": "Yes", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","-1","-1","0","ElectroPneumatic"], + ["1","-1","0.35","4","ElectroPneumatic"], + ["2","-1","-1","0","ElectroPneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "300"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "3900", "MinV": "2100", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "1"} +} diff --git a/tests/unit/data/fiz/impuls_v1/31we-c_pr.json b/tests/unit/data/fiz/impuls_v1/31we-c_pr.json new file mode 100644 index 000000000..0272b3e24 --- /dev/null +++ b/tests/unit/data/fiz/impuls_v1/31we-c_pr.json @@ -0,0 +1,20 @@ +{ + "_source": "pkp/impuls_v1/31we-c_pr.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "38700", "Vmax": "160", "PWR": "2", "SandCap": "0", "Type": "ezt"}, + "Load": {"MaxLoad": "46", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "15.92", "H": "4.12", "W": "2.96", "Cx": "0.1"}, + "Wheels": {"D": "0.84", "AIM": "500", "Tw": "1.435", "Axle": "1'2'", "Ad": "2.75", "Bd": "15.92", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "2", "MBF": "85.0", "MaxBP": "4.06", "MaxLBP": "4.06", "BCN": "6", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.01", "MinCP": "10.0", "MaxCP": "12.0", "CompressorSpeed": "0.0000001", "BVV": "187", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "3.09", "CompressorPower": "Converter"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.8", "PressOn": "4.5", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.21", "CloseSpeed": "0.19", "DoorCloseDelay": "2.1", "DoorOpenWithPermit": "1", "DoorMaxShiftL": "0.7", "DoorMaxShiftR": "0.7", "DoorOpenMethod": "Plug", "DoorMaxShiftPlug": "0.028", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorAutoCloseRemote": "Yes", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes"}, + "BuffCoupl": {"CType": "Articulated", "AllowedFlag": "-255", "ControlType": "impuls"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocBrakeHandle": "FD1", "MaxBPMass": "53", "BCPN": "2", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "DynamicBrake": "Passive", "MCPN": "10", "SCPN": "0", "AutoRelay": "Yes", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "Yes", "ASB": "Yes", "CompartmentLightsStart": "Manual", "HAO": "Yes", "OMP": "0.8", "PantEPValveStart": "Automatic", "PantEPValveSpring": "No", "PantCompressorStart": "Automatic", "PantAutoValve": "Yes", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","-1","-1","0","ElectroPneumatic"], + ["1","-1","0.35","4","ElectroPneumatic"], + ["2","-1","-1","0","ElectroPneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "300"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "1", "MaxVoltage": "3900", "MinV": "2100", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "PhysicalLayout": "2"} +} diff --git a/tests/unit/data/fiz/impuls_v1/31we-d_pr.json b/tests/unit/data/fiz/impuls_v1/31we-d_pr.json new file mode 100644 index 000000000..d78930b9b --- /dev/null +++ b/tests/unit/data/fiz/impuls_v1/31we-d_pr.json @@ -0,0 +1,50 @@ +{ + "_source": "pkp/impuls_v1/31we-d_pr.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "31800", "Vmax": "160", "PWR": "1000", "SandCap": "200", "Type": "ezt"}, + "Load": {"MaxLoad": "54", "LoadQ": "pieces", "LoadAccepted": "Passengers", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "1"}, + "Dimensions": {"L": "21.06", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "0.840", "AIM": "2000", "Tw": "1.435", "Axle": "Bo'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.37", "MaxLBP": "4.37", "BCN": "4", "BCR": "0.127", "BCD": "0.05", "BCM": "1.9075", "HiPP": "5", "LoPP": "3.5", "Vv": "0.125", "MinCP": "8.5", "MaxCP": "10.0", "MinCP_B": "7.5", "MaxCP_B": "9.0", "CompressorSpeed": "0.00625", "BVV": "125", "BM": "Cosid", "BCS": "1", "BSA": "0.00", "BRE": "0.95", "TareMaxBP": "3.85", "CompressorPower": "Converter", "LPOn": "2.8", "LPOff": "4.3", "UBB1": "2", "EmergencyCutsOffHandle": "Yes"}, + "SpringBrake": {"Volume": "10", "MBF": "2.9", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.21", "CloseSpeed": "0.19", "DoorCloseDelay": "2.1", "DoorOpenWithPermit": "1", "DoorMaxShiftL": "0.7", "DoorMaxShiftR": "0.7", "DoorOpenMethod": "Plug", "DoorMaxShiftPlug": "0.028", "DoorStayOpen": "5.0", "DoorClosureWarningAuto": "Yes", "DoorBlocked": "Yes", "DoorAutoCloseRemote": "Yes", "PlatformMaxShift": "0.270", "PlatformSpeed": "0.4", "PlatformOpenMethod": "Shift", "MirrorMaxShift": "45", "DoorNeedPermit": "Yes", "MirrorVelClose": "20", "DoorsPermitLightBlinking": "2"}, + "BuffCoupl1": {"CType": "Articulated", "AllowedFlag": "-255", "ControlType": "impuls"}, + "BuffCoupl2": {"CType": "Automatic", "kB": "4200", "DmaxB": "0.05", "FmaxB": "1740", "kC": "6800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "AutomaticFlag": "39", "ControlType": "31we_pr"}, + "Cntrl": {"BrakeHandle": "MHZ_6P", "LocBrakeHandle": "FD1", "BrakeSystem": "ElectroPneumatic", "MaxBPMass": "37.8", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "LocalBrake": "PneumaticBrake", "MCPN": "10", "SCPN": "1", "IniCDelay": "0.0", "SCDelay": "0.8", "CoupledCtrl": "No", "EIMCtrlAddZeros": "Yes", "SBD": "0.65", "ASB": "Yes", "ConverterStart": "Automatic", "ConverterStartDelay": "8", "MainInitTime": "2", "BrakeOpModes": "PN", "MotorBlowersStart": "Converter", "EIMCtrlEmergency": "Yes", "PantEPValveStart": "Automatic", "PantEPValveSpring": "No", "SpringBrakeCutsOffDrive": "No", "SpringBrakeDriveEmergencyVel": "2", "ReleaseParkingBySpringBrakeWhenDoorIsOpen": "Yes", "CompartmentLightsStart": "Manual", "HAO": "Yes", "OMP": "0.8", "AutomaticCabActivation": "No", "InactivaCabEmergencyBrake": "Yes", "InactiveCabFlag": "239", "rows": [ + ["-1","0.5","0.0","6","ElectroPneumatic"], + ["0","0.5","-1","4","ElectroPneumatic"], + ["1","0.5","0.35","4","ElectroPneumatic"], + ["2","0.5","0.35","4","Pneumatic"], + ["3","0.5","0.35","4","Pneumatic"], + ["4","-1","0.35","4","Pneumatic"], + ["5","0.34","-1","4","Pneumatic"], + ["6","-1","-1","0","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.1", "CabDependent": "Yes", "RadioStop": "Yes", "MagnetLocation": "4.5"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "300"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "0", "MaxVoltage": "3900", "MinV": "2100", "MaxCurrent": "1200", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4"}, + "Engine": {"EngineType": "ElectricInductionMotor", "InvNo": "2", "Trans": "60:252", "dfic": "794", "dfmax": "1.500", "p": "2", "cfu": "40.1", "cim": "12.24", "icif": "0.679", "Uzmax": "2200", "Uzh": "1500", "DU": "20", "I0": "10", "fcfu": "47.6", "fcfuh": "31.4", "F0": "100", "a1": "0", "Pmax": "1260", "Fh": "48", "Ph": "800", "Vh0": "5", "Vh1": "15", "Imax": "475", "abed": "0", "edep": "3.0", "eimclf": "Yes", "MotorBlowersSpeed": "-15", "MotorBlowersSustainTime": "300", "MotorBlowersStartVelocity": "0.1"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "10|20|40|60|80|100|120|140|160", "OverrideManual": "No", "InitPwr": "1.0", "MaxPwrVel": "-1", "StartVel": "-1", "VelStep": "10", "PwrStep": "0.0", "MinPwr": "0.2", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "160", "Offset": "-0.5", "kPpos": "0.2", "kPneg": "0.4", "kIpos": "0.00", "kIneg": "0.00", "BrakeIntervention": "No", "SpeedCtrlATOF": "2"}, + "AI": {"Pantstate": "1", "LocalBrakeAccFactor": "0.5", "IdlePantUp": "No"}, + "Blending": {"MED_amax": "1.08", "MED_EPVC": "No", "MED_Ncor": "No", "MED_FrEDFactor": "0", "MED_FirstDelayED": "0.5", "MED_ScndDelayED": "0.25"}, + "Switches": {"PantographPresets": "0|1|2|3"}, + "Circuit": {"ImaxHi": "500"}, + "LightsList": {"Size": "8", "Wrap": "No", "Default": "1", "rows": [ + ["0","0"], + ["388","34"], + ["21","34"], + ["1","16"], + ["16","1"], + ["22","34"], + ["38","38"], + ["34","34"] + ]}, + "ffList": {"Size": "5", "rows": [ + ["0","0.5"], + ["24","1.0"], + ["35","1.0"], + ["42","1.3"], + ["42","1.3"], + ["74","2.3"] + ]} +} diff --git a/tests/unit/data/fiz/road/man/nl293.json b/tests/unit/data/fiz/road/man/nl293.json new file mode 100644 index 000000000..3d5e91a44 --- /dev/null +++ b/tests/unit/data/fiz/road/man/nl293.json @@ -0,0 +1,36 @@ +{ + "_source": "road/man/nl293.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "road", "M": "12000", "Vmax": "90", "PWR": "206"}, + "Load": {"LoadAccepted": "Passengers,Luggage", "MaxLoad": "44", "LoadQ": "pieces", "OverLoadFactor": "1.2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "10.5", "H": "3", "W": "2.6", "Cx": "0.4"}, + "Wheels": {"D": "1.0", "AIM": "300", "Tw": "2.3", "Axle": "1'A", "Bd": "4.615", "Rmin": "10.5"}, + "Brake": {"BrakeType": "Hydraulic", "NBpA": "2", "MBF": "14.75", "MaxBP": "0.5", "BCN": "4", "BCR": "0.2", "BCD": "0.2", "BCM": "2.0"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "30", "CloseSpeed": "38", "DoorMaxShiftL": "88", "DoorMaxShiftR": "88", "DoorOpenMethod": "Fold", "DoorClosureWarning": "Yes"}, + "BuffCoupl": {"CType": "Bare"}, + "Cntrl": {"BrakeSystem": "Individual", "BCPN": "10", "LocalBrake": "HydraulicBrake", "MCPN": "10", "SCPN": "5", "IniCDelay": "1.0", "SCDelay": "0.25", "AutoRelay": "Yes", "OilStart": "Automatic", "FuelStart": "Automatic", "rows": []}, + "Light": {"Light": "Accu", "LMaxVoltage": "12", "LCap": "340", "LRS": "Generator", "LMaxCurrent": "10"}, + "Power": {"EnginePower": "Internal"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "10:54", "nmin": "600", "nmax": "2000", "nmax_cutoff": "1900", "AIM": "50", "EUS": "2.5", "EDS": "2.5", "IsTC": "Yes", "TC_TMMax": "1.52", "TC_CP": "0.85", "TC_LT": "1700", "TC_LR": "2.0", "TC_ULR": "2.0", "TC_FRI": "1", "TC_FRD": "1", "TC_TII": "1.63", "TC_TIO": "0", "TC_TOO": "0", "TC_LS": "33", "TC_ULS": "25.4", "IsRetarder": "Yes", "R_Place": "2", "R_TII": "2.0", "R_MT": "1000", "R_MP": "100", "R_FRI": "1", "R_FRD": "1", "R_MinVel": "30"}, + "MotorParamTable": {"minVelfullengage": "2.0", "engageDia": "1", "engageMaxForce": "1600", "engagefriction": "1", "rows": [ + ["0","0","0","-1","1"], + ["1","2.81","-1","18.8","1"], + ["2","1.84","15.2","28.6","1"], + ["3","1.36","25.4","38.8","1"], + ["4","1.00","34.6","52.7","1"], + ["5","0.80","43.2","200","1"] + ]}, + "DList": {"Size": "10", "nMmax": "20.83", "Mmax": "1400", "Mnmax": "1050", "nmax": "31.67", "nominalfill": "1", "Mstand": "150", "rows": [ + ["0","0.1","0"], + ["1","0.1","2"], + ["2","0.2","2"], + ["3","0.3","2"], + ["4","0.4","2"], + ["5","0.5","2"], + ["6","0.6","2"], + ["7","0.7","2"], + ["8","0.8","2"], + ["9","0.9","2"], + ["10","1.0","2"] + ]} +} diff --git a/tests/unit/data/fiz/sa134_v1/218mc_a.json b/tests/unit/data/fiz/sa134_v1/218mc_a.json new file mode 100644 index 000000000..6c91065df --- /dev/null +++ b/tests/unit/data/fiz/sa134_v1/218mc_a.json @@ -0,0 +1,94 @@ +{ + "_source": "pkp/sa134_v1/218mc_a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "41000", "Vmax": "120", "PWR": "382", "SandCap": "100", "Type": "dmu"}, + "Load": {"MaxLoad": "50", "OverLoadFactor": "2.2", "LoadQ": "pieces", "LoadAccepted": "passengers", "LoadSpeed": "0.5", "UnLoadSpeed": "0.7"}, + "Dimensions": {"L": "20.75", "H": "4.2", "W": "2.9", "Cx": "0.0"}, + "Wheels": {"D": "0.84", "Tw": "1.435", "AIM": "300", "Axle": "B'1'", "Ad": "2", "Bd": "17.23", "Rmin": "75", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "EStED", "Size": "150", "NBpA": "4", "MBF": "85.0", "MaxBP": "2.9", "LocMaxBP": "3.8", "RV": "-1", "RM": "0.77", "BCN": "6", "BCR": "0.127", "BCD": "0.1", "BCM": "2.15", "HiPP": "5", "LoPP": "3.5", "Vv": "0.50", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.0208", "CompressorPower": "Main", "BCS": "0.5", "BSA": "0", "BM": "Cosid", "BVV": "150", "BRE": "0.97", "TareMaxBP": "2.2", "LPOn": "3.0", "LPOff": "4.5", "UBB1": "2", "UBB2": "8"}, + "SpringBrake": {"Volume": "15", "MBF": "4.35", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "0.5", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "4"}, + "Doors": {"OpenCtrl": "Mixed", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.23", "CloseSpeed": "0.19", "DoorCloseDelay": "2.45", "DoorMaxShiftL": "0.78", "DoorMaxShiftR": "0.78", "DoorOpenMethod": "Plug", "DoorClosureWarning": "No", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.04", "MirrorMaxShift": "75", "DoorStayOpen": "10", "DoorNeedPermit": "Yes", "DoorPermitList": "1|0|2|3", "DoorPermitListDefault": "2", "DoorAutoCloseRemote": "No"}, + "BuffCoupl1": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "ControlType": "218M"}, + "BuffCoupl2": {"CType": "Articulated", "AllowedFlag": "-55", "ControlType": "218M"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "3", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Yes", "LocalBrake": "PneumaticBrake", "ManualBrake": "No", "MCPN": "5", "SCPN": "1", "AutoRelay": "Yes", "IniCDelay": "1.0", "SCDelay": "0.3", "BrakeDelays": "PR", "MaxBPMass": "55", "BrakeHandle": "MHZ_K5P", "LocBrakeHandle": "FD1", "WaterStart": "Battery", "OilStart": "Automatic", "FuelStart": "Automatic", "ConverterStart": "Automatic", "ConverterStartDelay": "4", "BrakeOpModes": "PN", "EIMCtrlType": "3", "DirChangeMaxPos": "4", "SBD": "0.47", "AutomaticCabActivation": "No", "InactiveCabFlag": "27", "CompartmentLightsStart": "Manual", "rows": [ + ["-1","0.6","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "2.4", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.1", "RadioStop": "Yes", "CabDependent": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "155"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "69:229", "TransEff": "0.90", "nmin": "850", "nmax": "2100", "nmax_cutoff": "2150", "nmin_hdrive": "1000", "nmin_hdrive_factor": "800", "nmin_retarder": "1400", "MaxVelANS": "255", "AIM": "150", "EUS": "2.5", "EDS": "2.5", "IsTC": "Yes", "TC_TMMax": "4.23", "TC_CP": "0.80", "TC_LT": "3000", "TC_LR": "1", "TC_ULR": "1", "TC_FRI": "0.25", "TC_FRD": "1", "TC_TII": "1.97", "TC_TIO": "0", "TC_TOO": "0", "TC_LS": "80", "TC_ULS": "75", "IsRetarder": "Yes", "R_Place": "0", "R_TII": "18.0", "R_MT": "2000", "R_MP": "300", "R_FRI": "1", "R_FRD": "1", "R_MinVel": "16", "R_EngageVel": "95", "WaterMaxTemperature": "105", "WaterCoolingTemperature": "95", "WaterFlowTemperature": "75", "WaterAuxCircuit": "No", "WaterCoolingFanSpeed": "-100", "NominalCoolingPower": "300"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "696", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "1500", "HGeneratorMaxVoltage": "3300"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|30|40|50|60|70|80|90|100|120", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "120", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "Yes"}, + "TurboPos": {"TurboPos": "1"}, + "MotorParamTable": {"minVelfullengage": "-0.1", "engageDia": "1", "engageMaxForce": "3300", "engagefriction": "5.0", "rows": [ + ["0","0","0","-1","1"], + ["1","0.72","-1","200","1"] + ]}, + "DList": {"Size": "10", "Mmax": "2750", "nMmax": "18.3", "nmax": "33.3", "Mnmax": "2142", "nominalfill": "1.0", "Mstand": "250.0", "NomFuelConsRate": "220", "rows": [ + ["0","0.15","0"], + ["1","0.10","2"], + ["2","0.20","2"], + ["3","0.30","2"], + ["4","0.40","2"], + ["5","0.50","2"], + ["6","0.60","2"], + ["7","0.70","2"], + ["8","0.80","2"], + ["9","0.90","2"], + ["10","1.00","2"] + ]}, + "DMList": {"rows": [ + ["0","0"], + ["80","591"], + ["400","738"], + ["800","1477"], + ["1300","2400"], + ["1500","2400"], + ["1750","2071"], + ["2000","1824"], + ["2200","1508"] + ]}, + "HTCList": {"rows": [ + ["0.000","4.89"], + ["0.061","4.44"], + ["0.159","3.63"], + ["0.236","3.12"], + ["0.293","2.81"], + ["0.354","2.46"], + ["0.427","2.14"], + ["0.492","1.87"], + ["0.565","1.62"], + ["0.642","1.39"], + ["0.720","1.23"], + ["0.800","1.00"], + ["1.000","0.00"] + ]}, + "UCList": {"Size": "5", "IntegratedBrakePN": "Yes", "IntegratedBrake": "Yes", "rows": [ + ["0","3","0","0","0","0.15","0.15","0","1","0"], + ["1","2","0","0","0","0.15","0.15","1","3","0"], + ["2","1","-1","0","-1","0.15","0.15","3","3","1"], + ["3","1","-1","1","0","0.00","0.15","3","4","1"], + ["4","0","-1","1","0","0.15","0.00","4","5","3"], + ["5","0","0","1","1","0.15","0.15","4","5","3"] + ]}, + "LightsList": {"Size": "5", "Wrap": "no", "Default": "1", "rows": [ + ["0","0"], + ["21","34"], + ["22","34"], + ["34","34"], + ["16","1"] + ]}, + "WiperList": {"Size": "5", "Angle": "-75", "Default": "1", "rows": [ + ["3","0.5","0.5","0.0"], + ["0","0.8","0.0","0.5"], + ["3","0.8","6.0","0.5"], + ["3","0.8","3.0","0.0"], + ["3","0.8","1.0","0.0"] + ]} +} diff --git a/tests/unit/data/fiz/sieciowiec_v1/a.json b/tests/unit/data/fiz/sieciowiec_v1/a.json new file mode 100644 index 000000000..4471406d7 --- /dev/null +++ b/tests/unit/data/fiz/sieciowiec_v1/a.json @@ -0,0 +1,13 @@ +{ + "_source": "pkp/sieciowiec_v1/a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "35500", "Vmax": "80", "HeatingP": "20", "LightP": "0.56"}, + "Load": {"MaxLoad": "80", "LoadQ": "pieces", "LoadAccepted": "Passengers,Luggage", "OverLoadFactor": "2", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "21.13", "H": "4", "W": "2.9", "Cx": "0.1"}, + "Wheels": {"D": "1.00", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.83", "Bd": "13.9", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeType": "None", "NBpA": "0", "MBF": "0", "MaxBP": "0"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "115"}, + "Cntrl": {"MCPN": "1", "BrakeSystem": "Individual", "LocalBrake": "NoBrake", "rows": []}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "495", "AlterLRS": "Generator"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000", "AlterHeating": "PowerCable", "AlterHPowerTrans": "SteamPower", "AlterHSteamPress": "0.45"} +} diff --git a/tests/unit/data/fiz/sm03_v1/sm03.json b/tests/unit/data/fiz/sm03_v1/sm03.json new file mode 100644 index 000000000..0b241f6b4 --- /dev/null +++ b/tests/unit/data/fiz/sm03_v1/sm03.json @@ -0,0 +1,41 @@ +{ + "_source": "pkp/sm03_v1/sm03.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "23200", "Vmax": "25.68", "PWR": "111.855", "SandCap": "200"}, + "Dimensions": {"L": "6.94", "H": "3.21", "W": "2.97", "Cx": "0.3"}, + "Wheels": {"D": "0.94", "Tw": "1.435", "AIM": "250", "Axle": "B", "Ad": "2.5", "Bd": "2.5", "Rmin": "50", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "K", "NBpA": "2", "MBF": "85.0", "MaxBP": "3.5", "LocMaxBP": "5.0", "BCN": "1", "BCR": "0.114", "BCD": "0.12", "BCM": "10.0", "HiPP": "5", "LoPP": "3.5", "Vv": "0.3", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.0125", "CompressorPower": "Main", "BCS": "0.8", "BSA": "0", "BM": "P10-Bg", "BVV": "40", "BRE": "0.9"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "3"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", "DynamicBrake": "Passive", "MCPN": "9", "SCPN": "7", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "P", "BrakeHandle": "Knorr", "LocBrakeHandle": "Knorr", "FuelStart": "Automatic", "ConverterStart": "Automatic", "rows": [ + ["-1","0.6","0","15","Pneumatic"], + ["0","0.5","0","9","Pneumatic"], + ["1","-1","-1","0","Pneumatic"], + ["2","-1","-1","5","Pneumatic"], + ["3","0.34","-1","9","Pneumatic"], + ["4","0.0","-1","15","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "100"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "1:1", "nmin": "900", "nmax": "1500", "nmax_cutoff": "1550", "AIM": "100", "OilMinPressure": "0.343", "OilMaxPressure": "0.686", "WaterCoolingTemperature": "80", "WaterShutters": "yes", "WaterAuxCircuit": "no", "HeatKFO": "160", "HeatKFO2": "180", "HeatKFS": "160", "HeatKFE": "7", "HeatKW": "2", "HeatKV": "0.3", "IsTC": "No", "TC_TMMax": "1.00", "TC_CP": "0.9", "TC_LT": "1000", "TC_LR": "1", "TC_ULR": "1", "TC_FRI": "1", "TC_FRD": "1", "TC_TII": "0.8", "TC_TIO": "0", "TC_TOO": "0", "TC_LS": "3.5", "TC_ULS": "3.0", "EUS": "0.35", "EDS": "1.0"}, + "MotorParamTable": {"minVelfullengage": "3.8", "engageDia": "1", "engageMaxForce": "800", "engagefriction": "1", "rows": [ + ["0","0","0","-1"], + ["1","46.88","-1","5.73"], + ["2","0","0","-1"], + ["3","28.65","2.9","9.42"], + ["4","0","0","-1"], + ["5","17.12","4.7","15.69"], + ["6","0","0","-1"], + ["7","10.46","10.6","100"] + ]}, + "DList": {"Size": "9", "Mmax": "784.5", "nMmax": "15", "Mnmax": "686.4", "nmax": "25", "nominalfill": "1", "Mstand": "200.0", "rows": [ + ["0","0.1","0"], + ["1","0.2","2"], + ["2","0.3","2"], + ["3","0.4","2"], + ["4","0.5","2"], + ["5","0.6","2"], + ["6","0.7","2"], + ["7","0.8","2"], + ["8","0.9","2"], + ["9","1.0","2"] + ]} +} diff --git a/tests/unit/data/fiz/sm31_v1/411d.json b/tests/unit/data/fiz/sm31_v1/411d.json new file mode 100644 index 000000000..b08d1c7e5 --- /dev/null +++ b/tests/unit/data/fiz/sm31_v1/411d.json @@ -0,0 +1,43 @@ +{ + "_source": "pkp/sm31_v1/411d.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "116400", "Mred": "15000", "Vmax": "80", "PWR": "880", "SandCap": "400"}, + "Dimensions": {"L": "17.00", "H": "4.292", "W": "2.904", "Cx": "0.3"}, + "Wheels": {"D": "1.100", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "2.6", "Bd": "13.1", "Rmin": "80"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.3", "LocMaxBP": "4.3", "MaxASBP": "1.3", "BCN": "12", "BCR": "0.114", "BCD": "0.06", "BCM": "5.84", "HiPP": "5", "LoPP": "3.5", "Vv": "0.8", "MinCP": "7.5", "MaxCP": "8.0", "CompressorSpeed": "0.03", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "CompressorPower": "Converter", "BVV": "130", "BRE": "0.9"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "35"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "11", "SCPN": "2", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GP", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "ConverterStart": "Automatic", "ConverterStartDelay": "7", "rows": [ + ["-1","0.60","-1","15","Pneumatic"], + ["0","0.50","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0.00","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "100"}, + "Engine": {"AIM": "1.6", "EngineType": "DumbDE", "Trans": "19:70", "Ftmax": "392000", "Vhyp": "4", "Vadd": "5", "Cr": "1", "RelayType": "1"}, + "TurboPos": {"TurboPos": "5"}, + "MotorParamTable": {"rows": [ + ["0","17.567","64","1500","15","30","0"], + ["1","16.29","123.7","1750","32","43","0"], + ["2","15","183.3","2000","49","200","0"] + ]}, + "Circuit": {"ImaxLo": "2800", "IminLo": "2000"}, + "WWList": {"Size": "11", "rows": [ + ["496","0","0","0"], + ["496","30","870","3000"], + ["496","75","870","3000"], + ["496","105","870","3000"], + ["496","135","870","3000"], + ["568","213","870","3000"], + ["640","305","870","3000"], + ["712","395","870","3000"], + ["784","495","870","3000"], + ["856","595","870","3000"], + ["928","705","870","3000"], + ["1000","800","870","3000"] + ]} +} diff --git a/tests/unit/data/fiz/sm40_v1/sm40.json b/tests/unit/data/fiz/sm40_v1/sm40.json new file mode 100644 index 000000000..e8cec2474 --- /dev/null +++ b/tests/unit/data/fiz/sm40_v1/sm40.json @@ -0,0 +1,48 @@ +{ + "_source": "pkp/sm40_v1/sm40.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "62000", "Mred": "10000", "Vmax": "80", "PWR": "1175", "SandCap": "400"}, + "Dimensions": {"L": "11.30", "H": "4.44", "W": "3.05", "Cx": "0.3"}, + "Wheels": {"D": "1.04", "AIM": "4000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "1.75", "Bd": "10.3", "Rmin": "100"}, + "Brake": {"BrakeValve": "K", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.0", "LocMaxBP": "5.0", "BCN": "4", "BCR": "0.236", "BCD": "0.1", "BCM": "6.54", "HiPP": "5", "LoPP": "3.5", "Vv": "0.50", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.0208", "CompressorPower": "Main", "BCS": "0.8", "BSA": "0", "BM": "P10-Bg", "BVV": "127", "BRE": "0.75"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "850", "beta": "0.5", "AllowedFlag": "39"}, + "Cntrl": {"ControlType": "sm40", "BrakeSystem": "Pneumatic", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "0", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", "MCPN": "20", "SCPN": "1", "AutoRelay": "Yes", "IniCDelay": "0.4", "SCDelay": "0.4", "BrakeDelays": "GP", "BrakeHandle": "Knorr", "LocBrakeHandle": "Knorr", "ConverterStart": "Automatic", "rows": [ + ["-1","0.90","0","15","Pneumatic"], + ["0","0.50","0","9","Pneumatic"], + ["1","-1","-1","0","Pneumatic"], + ["2","-1","-1","5","Pneumatic"], + ["3","0.001","-1","9","Pneumatic"], + ["4","0.00","-1","15","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "100"}, + "Engine": {"EngineType": "DieselElectric", "Trans": "17:77", "Ftmax": "184500", "Flat": "1", "Vhyp": "5.5", "Vadd": "5.5", "Cr": "1.0", "RelayType": "41"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "MotorParamTable": {"rows": [ + ["0","5.84","137","300","91","25","0"], + ["1","3.32","191","400","230","0","2700"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "3350", "ImaxHi": "3350", "IminLo": "400", "IminHi": "400"}, + "WWList": {"Size": "20", "rows": [ + ["470","0","0","0"], + ["900","0","0","0"], + ["900","0","0","0"], + ["800","75","360","2600"], + ["800","100","405","2600"], + ["800","125","450","2600"], + ["800","145","450","2600"], + ["845","180","495","2600"], + ["890","230","540","2600"], + ["930","270","585","2600"], + ["970","315","630","2600"], + ["1005","355","675","2600"], + ["1100","360","410","2600"], + ["1100","365","285","2600"], + ["1100","370","405","2600"], + ["1100","375","440","2600"], + ["1100","380","468","2600"], + ["1100","385","490","2600"], + ["1100","390","502","2600"], + ["1100","400","512","2600"], + ["1100","412","540","2600"] + ]} +} diff --git a/tests/unit/data/fiz/sm42_v1/6d1.json b/tests/unit/data/fiz/sm42_v1/6d1.json new file mode 100644 index 000000000..54bdf251b --- /dev/null +++ b/tests/unit/data/fiz/sm42_v1/6d1.json @@ -0,0 +1,42 @@ +{ + "_source": "pkp/sm42_v1/6d1.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "74000", "Mred": "10000", "Vmax": "90", "PWR": "590", "SandCap": "300"}, + "Dimensions": {"L": "14.24", "H": "4.285", "W": "3.170", "Cx": "0.2"}, + "Wheels": {"D": "1.100", "AIM": "4000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.6", "Bd": "7.524", "Rmin": "80"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.0", "MaxASBP": "1.8", "BCN": "4", "BCR": "0.1143", "BCD": "0.05", "BCM": "6.68", "HiPP": "5.0", "LoPP": "3.5", "Vv": "0.8", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.057", "CompressorPower": "Engine", "BVV": "200", "BRE": "0.85"}, + "BuffCoupl": {"ControlType": "sm42", "CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "3", "BDelay3": "36", "BDelay4": "22", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "11", "SCPN": "1", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GP", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "ManualBrake": "Yes", "OilStart": "Mixed", "WaterStart": "Battery", "ConverterStart": "Automatic", "rows": [ + ["-1","0.7","-1","15","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.45","-1","9","Pneumatic"], + ["2","0.41","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.28","-1","9","Pneumatic"], + ["6","0","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active", "AwareMinSpeed": "0.0", "AwareDelay": "60", "SoundSignalDelay": "2.5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "2.5", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "91"}, + "Engine": {"AIM": "1.6", "RPMDecRate": "4.0", "EngineType": "DumbDE", "Trans": "17:75", "Ftmax": "228000", "Vhyp": "7", "Vadd": "5.5", "Cr": "1", "ShuntMode": "1", "WaterCoolingTemperature": "81", "WaterCoolingFanSpeed": "-1200", "WaterMaxTemperature": "85", "OilMinPressure": "0.15", "OilMinTemperature": "25", "OilMaxTemperature": "90", "HeaterMinTemperature": "55", "HeaterMaxTemperature": "60", "HeatKFS": "35", "HeatKFO": "30", "HeatKFE": "7", "HeatKW": "0.011", "HeatKV": "0.3", "HeatKFO2": "100"}, + "TurboPos": {"TurboPos": "5"}, + "MotorParamTable": {"rows": [ + ["0","17.567","64","1500","15","0","0"], + ["1","15","183.3","2000","49","0","0"] + ]}, + "Circuit": {"CircuitRes": "0.1", "ImaxLo": "1900", "ImaxHi": "1900", "IminLo": "400", "IminHi": "400"}, + "WWList": {"Size": "11", "rows": [ + ["496","0","0","0","0","0","0"], + ["496","20","125","600","0","140","180"], + ["496","50","156","846","0","140","180"], + ["496","90","194","1065","0","140","180"], + ["496","135","240","1260","0","140","180"], + ["568","182","293","1428","25","150","200"], + ["640","234","353","1571","50","160","235"], + ["712","286","420","1688","80","200","270"], + ["784","348","494","1780","100","235","305"], + ["856","408","575","1845","130","300","370"], + ["928","474","664","1886","150","360","410"], + ["1000","540","760","1900","170","450","490"] + ]} +} diff --git a/tests/unit/data/fiz/sn61_v2/sn61.json b/tests/unit/data/fiz/sn61_v2/sn61.json new file mode 100644 index 000000000..4237a1336 --- /dev/null +++ b/tests/unit/data/fiz/sn61_v2/sn61.json @@ -0,0 +1,42 @@ +{ + "_source": "pkp/sn61_v2/sn61.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "Type": "sn61", "M": "54810", "Vmax": "100", "PWR": "368", "SandCap": "100"}, + "Load": {"MaxLoad": "50", "LoadQ": "pieces", "LoadAccepted": "passengers", "LoadSpeed": "1", "UnLoadSpeed": "1"}, + "Dimensions": {"L": "24.22", "H": "4.12", "W": "2.96", "Cx": "0.5"}, + "Wheels": {"D": "0.94", "Tw": "1.435", "AIM": "300", "Axle": "B'2'", "Ad": "3.2", "Bd": "18.36", "Rmin": "100", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "ESt3d_PZZ", "Size": "150", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.8", "LocMaxBP": "3.8", "BCN": "3", "BCR": "0.127", "BCD": "0.1", "BCM": "6.54", "HiPP": "5", "LoPP": "3.5", "Vv": "0.50", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.0208", "CompressorPower": "Main", "BCS": "1.5", "BSA": "2", "BM": "P10-Bg", "BVV": "150", "BRE": "0.85"}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "Passenger", "OpenSpeed": "110", "CloseSpeed": "400", "DoorMaxShiftL": "89", "DoorMaxShiftR": "89", "DoorOpenMethod": "Roll", "DoorClosureWarning": "No"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "ControlType": "sn61"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "DirChangeMaxPos": "2", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", "MCPN": "8", "SCPN": "5", "AutoRelay": "Yes", "IniCDelay": "1.5", "SCDelay": "0.3", "BrakeDelays": "P", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "FuelStart": "Automatic", "WaterStart": "Battery", "OilStart": "Automatic", "ConverterStart": "Automatic", "rows": [ + ["-1","0.6","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "39:83", "nmin": "660", "nmax": "1350", "nmax_cutoff": "1250", "AIM": "150"}, + "MotorParamTable": {"minVelfullengage": "7.8", "engageDia": "1", "engageMaxForce": "3800", "engagefriction": "1", "rows": [ + ["0","0","0","-1","1"], + ["1","5.056","-1","20.3","1"], + ["2","3.325","14.7","30.8","1"], + ["3","2.259","25.4","45.3","1"], + ["4","1.486","40.9","68.9","1"], + ["5","1.000","62.4","200","1"] + ]}, + "DList": {"Size": "8", "Mmax": "3630", "nMmax": "13.333", "Mnmax": "3100", "nmax": "21", "nominalfill": "0.9", "Mstand": "300.0", "rows": [ + ["0","0","0"], + ["1","0.4","0"], + ["2","0.1","0"], + ["3","0.6","1"], + ["4","0.15","3"], + ["5","0.40","3"], + ["6","0.60","3"], + ["7","0.80","3"], + ["8","1.00","4"] + ]} +} diff --git a/tests/unit/data/fiz/st44_v2/m62-1151.json b/tests/unit/data/fiz/st44_v2/m62-1151.json new file mode 100644 index 000000000..1e75cc9c8 --- /dev/null +++ b/tests/unit/data/fiz/st44_v2/m62-1151.json @@ -0,0 +1,56 @@ +{ + "_source": "pkp/st44_v2/m62-1151.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "116000", "Mred": "20000", "Vmax": "100", "PWR": "1232", "SandCap": "400", "Type": "st44"}, + "Dimensions": {"L": "17.55", "H": "4.62", "W": "2.95", "Cx": "0.1"}, + "Wheels": {"D": "1.05", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "2.6", "Bd": "8.6", "Rmin": "80"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "124", "MaxBP": "4.1", "MaxLBP": "3.9", "MaxASBP": "1.8", "BCN": "8", "BCR": "0.1016", "BCD": "0.1", "BCM": "10.4", "HiPP": "5", "LoPP": "3.5", "Vv": "0.888", "MinCP": "7", "MaxCP": "8.5", "CompressorSpeed": "0.077", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "BVV": "222", "CompressorPower": "Main", "BRE": "0.9", "UBB1": "1", "UBB2": "2"}, + "BuffCoupl": {"ControlType": "kolomna", "CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "15", "SCPN": "2", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GP", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "AutoRelay": "Yes", "ManualBrake": "Yes", "OilStart": "Automatic", "WaterStart": "Battery", "ConverterStart": "Automatic", "rows": [ + ["-1","0.7","-1","15","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.29","-1","9","Pneumatic"], + ["6","0","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "100"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "400", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "750", "HGeneratorMaxVoltage": "3300"}, + "Engine": {"AIM": "3.3", "EngineType": "DieselElectric", "Trans": "15:68", "Ftmax": "396000", "Flat": "1", "Vhyp": "18", "Vadd": "0", "Cr": "1.107", "WaterFlowTemperature": "80", "WaterCoolingTemperature": "45", "WaterMaxTemperature": "95", "WaterShutters": "Yes", "WaterAuxCircuit": "Yes", "WaterAuxCoolingTemperature": "60", "WaterAuxMaxTemperature": "75", "WaterAuxShutters": "Yes", "OilMinPressure": "0.25", "OilMaxPressure": "0.8", "OilMaxTemperature": "71", "HeatKFO": "40", "HeatKFO2": "190", "HeatKW": "0.2", "WaterCoolingFanSpeed": "1.86"}, + "LightsList": {"Size": "8", "Wrap": "No", "Default": "4", "rows": [ + ["38","38","/","A"], + ["38","38","/","A"], + ["34","34"], + ["0","0"], + ["21","34"], + ["1","16"], + ["16","1"], + ["22","34"] + ]}, + "MotorParamTable": {"rows": [ + ["0","10","179","2000","72","2450","0"], + ["1","8","190","1600","95","2250","3600"], + ["2","7","300","1400","170","0","3450"] + ]}, + "WWList": {"Size": "15", "rows": [ + ["400","0","0","0"], + ["400","40","180","1500"], + ["425","100","240","2000"], + ["450","250","300","2080"], + ["475","450","330","3660"], + ["500","500","360","3830"], + ["525","550","380","4000"], + ["550","600","405","4160"], + ["575","700","430","4330"], + ["600","750","450","4660"], + ["625","800","475","4830"], + ["650","850","500","5160"], + ["675","950","525","5330"], + ["700","1050","550","5500"], + ["725","1150","575","5660"], + ["750","1232","600","6000"] + ]} +} diff --git a/tests/unit/data/fiz/st45_v2/301dd.json b/tests/unit/data/fiz/st45_v2/301dd.json new file mode 100644 index 000000000..bc1924c07 --- /dev/null +++ b/tests/unit/data/fiz/st45_v2/301dd.json @@ -0,0 +1,55 @@ +{ + "_source": "pkp/st45_v2/301dd.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "97000", "Mred": "15000", "Vmax": "120", "PWR": "1330", "SandCap": "400"}, + "Dimensions": {"L": "18.99", "H": "4.243", "W": "3.034", "Cx": "0.3"}, + "Wheels": {"D": "1.100", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "1.950", "Bd": "10.3", "Rmin": "100"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.1", "MaxASBP": "1.5", "RM": "0.71", "BCN": "8", "BCR": "0.114", "RV": "50", "BCD": "0.06", "BCM": "8.1", "HiPP": "5", "LoPP": "3.5", "Vv": "0.8", "MinCP": "8.0", "MaxCP": "10.0", "CompressorSpeed": "0.0613", "CompressorPower": "Converter", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "BVV": "250", "BRE": "0.9", "UBB1": "8", "HandlePipeUnlockPos": "-0.5", "LPOn": "2.75", "LPOff": "4.5"}, + "SpringBrake": {"Volume": "10", "MBF": "8", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "3.5", "ValveOnArea": "0.0002", "ValveOffArea": "0.0002", "ValvePNBArea": "0.002", "MTC": "4"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "39", "ControlType": "ST45"}, + "Cntrl": {"BrakeHandle": "MHZ_K8P", "LocBrakeHandle": "FD1", "BrakeSystem": "Pneumatic", "BCPN": "10", "BDelay1": "15", "BDelay2": "6", "BDelay3": "36", "BDelay4": "32", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "4", "SCPN": "3", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GPR", "ConverterStart": "Automatic", "ConverterStartDelay": "5", "MotorBlowersStart": "Converter", "FuelStart": "Automatic", "OilStart": "Automatic", "WaterStart": "Battery", "EIMCtrlType": "3", "BrakeOpModes": "PN", "DirChangeMaxPos": "2", "rows": [ + ["-1","0.7","-1","15","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.29","-1","9","Pneumatic"], + ["6","0","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "227.8"}, + "Engine": {"AIM": "0.9", "EngineType": "DumbDE", "Trans": "19:64", "Ftmax": "326000", "Flat": "0", "Vhyp": "7.2", "Vadd": "5.5", "Cr": "1.0", "RelayType": "45", "WaterMinTemperature": "40", "WaterFlowTemperature": "70", "WaterCoolingTemperature": "82", "WaterMaxTemperature": "91", "WaterShutters": "Yes", "WaterAuxCircuit": "Yes", "WaterAuxCoolingTemperature": "60", "WaterAuxMaxTemperature": "74", "WaterAuxShutters": "Yes", "OilMinPressure": "0.15", "HeatKFO2": "24", "HeatingRPM": "1096", "MotorBlowersSpeed": "-10", "MotorBlowersSustainTime": "600", "MotorBlowersStartVelocity": "0.1"}, + "SpeedControl": {"SpeedCtrl": "Yes", "SpeedButtons": "0|30|40|50|60|70|80|90|100|120", "OverrideManual": "Yes", "InitPwr": "0.3", "MaxPwrVel": "5", "StartVel": "2", "VelStep": "5", "PwrStep": "0.1", "MinPwr": "0.3", "MaxPwr": "1.0", "MinVel": "0", "MaxVel": "120", "Offset": "-0.5", "kPpos": "0.1", "kPneg": "0.4", "kIpos": "0.06", "kIneg": "0.06", "BrakeIntervention": "Yes"}, + "MotorParamTable": {"rows": [ + ["0","20","100","3000","20.8","1340","1713"], + ["1","18.5","110","2700","25","1340","1713"], + ["2","17.2","135","2500","30","1340","1713"], + ["3","14.3","170","2000","48.5","1340","1713"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "3300", "IminLo": "2500"}, + "WWList": {"Size": "4", "rows": [ + ["600","0","0","0"], + ["1365","1131","816","3026"], + ["1410","1196","864","3204"], + ["1455","1264","912","3382"], + ["1500","1320","960","3560"] + ]}, + "LightsList": {"Size": "8", "Wrap": "no", "Default": "4", "rows": [ + ["38","34"], + ["38","0"], + ["34","34"], + ["0","0"], + ["21","34"], + ["22","34"], + ["01","16"], + ["16","01"] + ]}, + "UCList": {"Size": "4", "IntegratedBrakePN": "no", "IntegratedBrake": "no", "rows": [ + ["0","0","0","0","0","0.15","0.15","0","1","0"], + ["1","0","0","0","0","0.15","0.15","3","3","1"], + ["2","0","0","1","0","0.00","0.15","3","4","1"], + ["3","0","0","1","0","0.15","0.00","4","5","3"], + ["4","0","0","1","1","0.15","0.15","4","5","3"] + ]} +} diff --git a/tests/unit/data/fiz/static/sug/sug_kielce.json b/tests/unit/data/fiz/static/sug/sug_kielce.json new file mode 100644 index 000000000..d7474ed0f --- /dev/null +++ b/tests/unit/data/fiz/static/sug/sug_kielce.json @@ -0,0 +1,70 @@ +{ + "_source": "static/sug/sug_kielce.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "120000", "Mred": "18000", "Vmax": "100", "PWR": "1860", "SandCap": "700", "Type": "et22"}, + "Load": {"MaxLoad": "52", "LoadQ": "pieces", "LoadAccepted": "pantstate", "LoadSpeed": "1", "UnLoadSpeed": "0.1"}, + "Dimensions": {"L": "16.820", "H": "4.559", "W": "3.005", "Cx": "0.585"}, + "Wheels": {"D": "1.250", "AIM": "2000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "2.1", "Bd": "8.6", "Rmin": "100", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "W", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.1", "LocMaxBP": "5.0", "BCN": "8", "BCR": "0.114", "BCD": "0.12", "BCM": "7.0", "HiPP": "5", "LoPP": "3.5", "Vv": "0.88", "MinCP": "7.0", "MaxCP": "8.0", "CompressorSpeed": "0.0567", "BCS": "0.8", "BSA": "0", "BM": "P10-Bg", "BVV": "200", "CompressorPower": "Main", "BRE": "0.75"}, + "Doors": {"OpenCtrl": "DriverCtrl", "CloseCtrl": "DriverCtrl", "OpenSpeed": "0.3", "CloseSpeed": "0.1", "DoorMaxShiftL": "0.53", "DoorMaxShiftR": "0.53", "DoorOpenMethod": "Shift", "DoorClosureWarning": "Yes"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "850", "beta": "0.5", "AllowedFlag": "0"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "4", "BDelay1": "15", "BDelay2": "4", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "ManualBrake": "Yes", "DynamicBrake": "Passive", "MCPN": "36", "SCPN": "3", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.23", "BrakeDelays": "GP", "BrakeHandle": "Knorr", "LocBrakeHandle": "Knorr", "rows": [ + ["-1","0.90","0","15","Pneumatic"], + ["0","0.50","0","9","Pneumatic"], + ["1","-1","-1","0","Pneumatic"], + ["2","-1","-1","5","Pneumatic"], + ["3","0.001","-1","9","Pneumatic"], + ["4","0.00","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Power": {"EnginePower": "CurrentCollector", "CollectorsNo": "2", "MaxVoltage": "3600", "MaxCurrent": "1500", "MinH": "0.38", "MaxH": "2.4", "CSW": "1.4", "MinV": "1800", "MinPress": "3.5", "MaxPress": "5.0"}, + "Engine": {"EngineType": "ElectricSeriesMotor", "Volt": "1500", "Trans": "24:85", "WindingRes": "0.1782", "nmax": "1500"}, + "Switches": {"Pantograph": "impulse"}, + "MotorParamTable0": {"rows": [ + ["0","24.84","85","0.02","167.8","85","0.02"], + ["1","21.37","180","-0.15","144.4","180","-0.15"], + ["2","20.67","230","-0.15","139.7","230","-0.15"], + ["3","21.69","190","-0.04","145","190","-0.04"] + ]}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "500", "ImaxHi": "500", "IminLo": "250", "IminHi": "250"}, + "RList": {"Size": "36", "RVent": "Automatic", "RVentnmax": "3300", "RVentCutOff": "50", "rows": [ + ["0","0","0","0","0"], + ["1","26.43","1","6","0"], + ["2","18.43","1","6","0"], + ["3","13.49","1","6","0"], + ["4","10.87","1","6","0"], + ["5","9.40","1","6","0"], + ["6","7.97","1","6","0"], + ["7","6.56","1","6","0"], + ["8","5.56","1","6","0"], + ["9","4.59","1","6","0"], + ["10","3.36","1","6","0"], + ["11","2.67","1","6","0"], + ["12","2.04","1","6","0"], + ["13","1.48","1","6","0"], + ["14","0.91","1","6","0"], + ["15","0.37","1","6","0"], + ["16","0.00","1","6","0"], + ["17","3.72","2","3","0"], + ["18","2.68","2","3","0"], + ["19","2.40","2","3","0"], + ["20","1.98","2","3","0"], + ["21","1.88","2","3","0"], + ["22","1.26","2","3","0"], + ["23","0.98","2","3","0"], + ["24","0.72","2","3","0"], + ["25","0.40","2","3","0"], + ["26","0.21","2","3","0"], + ["27","0.00","2","3","0"], + ["28","1.59","3","2","0"], + ["29","1.16","3","2","0"], + ["30","0.86","3","2","0"], + ["31","0.65","3","2","0"], + ["32","0.48","3","2","0"], + ["33","0.40","3","2","0"], + ["34","0.25","3","2","0"], + ["35","0.14","3","2","0"], + ["36","0.00","3","2","0"] + ]} +} diff --git a/tests/unit/data/fiz/su45_v2/301d.json b/tests/unit/data/fiz/su45_v2/301d.json new file mode 100644 index 000000000..e45090fb2 --- /dev/null +++ b/tests/unit/data/fiz/su45_v2/301d.json @@ -0,0 +1,47 @@ +{ + "_source": "pkp/su45_v2/301d.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "102000", "Mred": "15000", "Vmax": "120", "PWR": "1250", "SandCap": "400", "HeatingP": "300"}, + "Dimensions": {"L": "18.99", "H": "4.238", "W": "2.900", "Cx": "0.2"}, + "Wheels": {"D": "1.100", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "1.950", "Bd": "10.3", "Rmin": "100"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.1", "LocMaxBP": "4.5", "MaxASBP": "1.5", "RM": "0.69", "BCN": "8", "BCR": "0.114", "BCD": "0.06", "BCM": "8.1", "HiPP": "5", "LoPP": "3.5", "Vv": "0.8", "MinCP": "7.0", "MaxCP": "8.5", "CompressorSpeed": "0.053", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "CompressorPower": "Engine", "BVV": "240", "BRE": "0.9"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103", "ControlType": "SU45"}, + "Cntrl": {"BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "6", "BDelay3": "36", "BDelay4": "32", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "14", "SCPN": "3", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GPR", "MotorBlowersStart": "Manual", "ConverterStart": "Automatic", "rows": [ + ["-1","0.7","-1","15","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.29","-1","9","Pneumatic"], + ["6","0","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "227.8"}, + "TurboPos": {"TurboPos": "4"}, + "Engine": {"AIM": "1.6", "EngineType": "DumbDE", "Trans": "18:62", "Ftmax": "326000", "Flat": "0", "Vhyp": "7.2", "Vadd": "5.5", "Cr": "1.0", "RelayType": "45", "WaterMinTemperature": "40", "WaterFlowTemperature": "70", "WaterCoolingTemperature": "82", "WaterMaxTemperature": "91", "WaterShutters": "Yes", "WaterAuxCircuit": "Yes", "WaterAuxCoolingTemperature": "60", "WaterAuxMaxTemperature": "74", "WaterAuxShutters": "Yes", "OilMinPressure": "0.15", "HeatKFO2": "24", "HeatingRPM": "1096", "MotorBlowersSpeed": "-10"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "696", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "1500", "HGeneratorMaxVoltage": "3300"}, + "MotorParamTable": {"rows": [ + ["0","20","100","3000","20.8","1340","0"], + ["1","18.5","110","2700","25","1340","40"], + ["2","17.2","135","2500","30","1340","50"], + ["3","14.3","170","2000","48.5","1340","1713"] + ]}, + "WWList": {"Size": "14", "rows": [ + ["696","0","0","0"], + ["696","0","0","0"], + ["629","96","78","273"], + ["763","192","147","547"], + ["830","288","221","820"], + ["897","384","295","1094"], + ["964","480","369","1367"], + ["1031","576","442","1641"], + ["1098","672","516","1914"], + ["1165","768","590","2187"], + ["1232","864","664","2461"], + ["1299","960","737","2734"], + ["1366","1056","811","3007"], + ["1433","1152","885","3281"], + ["1500","1245","960","3560"] + ]} +} diff --git a/tests/unit/data/fiz/su46_v2/303d.json b/tests/unit/data/fiz/su46_v2/303d.json new file mode 100644 index 000000000..57061902f --- /dev/null +++ b/tests/unit/data/fiz/su46_v2/303d.json @@ -0,0 +1,50 @@ +{ + "_source": "pkp/su46_v2/303d.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "105000", "Mred": "15000", "Vmax": "120", "PWR": "1400", "SandCap": "400", "HeatingP": "440"}, + "Dimensions": {"L": "18.99", "H": "4.292", "W": "2.904", "Cx": "0.3"}, + "Wheels": {"D": "1.100", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "1.75", "Bd": "10.3", "Rmin": "100"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "4.4", "LocMaxBP": "4.4", "MaxASBP": "1.3", "RM": "0.71", "BCN": "12", "BCR": "0.114", "BCD": "0.06", "BCM": "5.84", "HiPP": "5", "LoPP": "3.5", "Vv": "0.9", "MinCP": "7", "MaxCP": "8.5", "CompressorSpeed": "0.05", "BCS": "1", "BSA": "2", "BM": "P10-Bgu", "CompressorPower": "Converter", "BVV": "200", "BRE": "0.8"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "103", "ControlType": "SU46"}, + "Cntrl": {"BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "6", "BDelay3": "36", "BDelay4": "32", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "15", "SCPN": "4", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GPR", "ConverterStart": "Automatic", "ConverterStartDelay": "10", "MotorBlowersStart": "Manual", "rows": [ + ["-1","0.7","-1","15","Pneumatic"], + ["0","0.5","-1","9","Pneumatic"], + ["1","0.46","-1","9","Pneumatic"], + ["2","0.42","-1","9","Pneumatic"], + ["3","0.38","-1","9","Pneumatic"], + ["4","0.34","-1","9","Pneumatic"], + ["5","0.29","-1","9","Pneumatic"], + ["6","0","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "100"}, + "Engine": {"AIM": "1.6", "EngineType": "DumbDE", "Trans": "18:62", "Ftmax": "317000", "Flat": "0", "Vhyp": "15.0", "Vadd": "3.8", "Cr": "1.0", "RelayType": "46", "HeatingRPM": "1098", "OilMinPressure": "0.055", "OilMaxPressure": "0.6", "WaterMinTemperature": "40", "WaterMaxTemperature": "92", "WaterCoolingTemperature": "76", "WaterFlowTemperature": "71", "WaterShutters": "Yes", "WaterAuxCircuit": "Yes", "WaterAuxFlowTemperature": "1", "WaterAuxMinTemperature": "35", "WaterAuxMaxTemperature": "71", "WaterAuxCoolingTemperature": "56", "WaterAuxShutters": "Yes", "HeaterMinTemperature": "50", "HeaterMaxTemperature": "85", "HeatKW": "0.068599", "HeatKV": "0.3", "HeatKFE": "1", "HeatKFS": "87.76269", "HeatKFO": "26.72716", "HeatKFO2": "57.72", "WaterCoolingFanSpeed": "0.56", "MotorBlowersSpeed": "-10"}, + "TurboPos": {"TurboPos": "4"}, + "Circuit": {"CircuitRes": "0.01", "ImaxLo": "3300", "IminLo": "2500"}, + "Clima": {"HMaxVoltage": "3000", "Heating": "Generator", "HGeneratorEngine": "Main", "HGeneratorMinRPM": "696", "HGeneratorMinVoltage": "2400", "HGeneratorMaxRPM": "1500", "HGeneratorMaxVoltage": "3300"}, + "MotorParamTable": {"rows": [ + ["0","16.7","100","2000","24.1","1680","0"], + ["1","16.9","155","2250","36.9","40","40"], + ["2","17.1","210","2500","50.8","1680","1680"], + ["3","17.3","265","2800","65.0","50","50"], + ["4","17.5","321","3125","79.9","0","1680"] + ]}, + "WWList": {"Size": "15", "rows": [ + ["696","0","0","0"], + ["696","0","0","0"], + ["696","80","350","1000"], + ["696","80","350","1000"], + ["763","214","455","1400"], + ["830","321","500","1600"], + ["897","428","545","1800"], + ["964","535","590","2000"], + ["1031","642","635","2200"], + ["1098","748","680","2400"], + ["1165","855","725","2600"], + ["1232","962","770","2800"], + ["1299","1069","815","3000"], + ["1366","1176","865","3200"], + ["1433","1283","910","3400"], + ["1500","1390","960","3600"] + ]} +} diff --git a/tests/unit/data/fiz/tem2_v2/tem2a.json b/tests/unit/data/fiz/tem2_v2/tem2a.json new file mode 100644 index 000000000..6a9831b57 --- /dev/null +++ b/tests/unit/data/fiz/tem2_v2/tem2a.json @@ -0,0 +1,39 @@ +{ + "_source": "pkp/tem2_v2/tem2a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "116000", "Mred": "20000", "Vmax": "100", "PWR": "780", "SandCap": "2000"}, + "Dimensions": {"L": "17", "H": "4.62", "W": "2.95", "Cx": "0.1"}, + "Wheels": {"D": "1.05", "AIM": "4000", "Tw": "1.435", "Axle": "Co'Co'", "Ad": "2.1", "Bd": "8.6", "Rmin": "80", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "2", "MBF": "124", "MaxBP": "4.1", "MaxLBP": "3.9", "BCN": "4", "BCR": "0.127", "BCD": "0.1", "BCM": "10.77", "HiPP": "5.0", "LoPP": "3.5", "Vv": "0.888", "MinCP": "7", "MaxCP": "8.5", "CompressorSpeed": "0.077", "CompressorPower": "Main", "BCS": "1", "BSA": "0", "BM": "P10-Bgu", "BVV": "222", "BRE": "0.85"}, + "BuffCoupl1": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "3"}, + "BuffCoupl2": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "23", "ControlType": "TEM2"}, + "Cntrl": {"BrakeHandle": "M394", "LocBrakeHandle": "FD1", "BrakeDelays": "G", "BrakeSystem": "Pneumatic", "BCPN": "5", "BDelay1": "25", "BDelay2": "13", "BDelay3": "25", "BDelay4": "13", "ASB": "None", "LocalBrake": "PneumaticBrake", "MCPN": "8", "SCPN": "2", "AutoRelay": "Yes", "IniCDelay": "0.5", "SCDelay": "0.1", "rows": [ + ["-1","1.0","-1","10","Pneumatic"], + ["0","0.5","-1","4","Pneumatic"], + ["1","-1","-1","0","Pneumatic"], + ["2","-1","-1","2","Pneumatic"], + ["3","0.0","-1","2","Pneumatic"], + ["4","0.0","-1","6","Pneumatic"], + ["5","0.0","-1","15","Pneumatic"] + ]}, + "Security": {"AwareSystem": "Active,CabSignal", "AwareDelay": "60", "SoundSignalDelay": "5", "MaxHoldTime": "1.0", "EmergencyBrakeDelay": "6", "RadioStop": "Yes"}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "75", "LMaxCurrent": "75"}, + "TurboPos": {"TurboPos": "3"}, + "Engine": {"AIM": "0.9", "EngineType": "DieselElectric", "Trans": "15:68", "Ftmax": "396000", "Flat": "1", "Vhyp": "15", "Vadd": "0", "Cr": "1.107", "OilMinPressure": "0.16", "OilMaxPressure": "0.3", "WaterCoolingFanSpeed": "1.41", "WaterMaxTemperature": "88", "WaterCoolingTemperature": "76", "WaterShutters": "yes"}, + "MotorParamTable": {"rows": [ + ["0","24","66","2000","14","1000","0"], + ["1","18","70","1600","17","1000","1400"], + ["2","15","106","1400","26","0","1400"] + ]}, + "WWList": {"Size": "8", "rows": [ + ["300","0","0","0"], + ["290","65","870","1900"], + ["270","166","870","1900"], + ["330","268","870","1900"], + ["400","370","870","1900"], + ["480","471","870","1900"], + ["570","573","870","1900"], + ["650","647","870","1900"], + ["750","780","870","1900"] + ]} +} diff --git a/tests/unit/data/fiz/wm15_v1/wm15.json b/tests/unit/data/fiz/wm15_v1/wm15.json new file mode 100644 index 000000000..1b1cdce49 --- /dev/null +++ b/tests/unit/data/fiz/wm15_v1/wm15.json @@ -0,0 +1,22 @@ +{ + "_source": "pkp/wm15_v1/wm15.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "20000", "Vmax": "80", "PWR": "200", "SandCap": "400"}, + "Dimensions": {"L": "11", "H": "4.292", "W": "2.900", "Cx": "0.3"}, + "Wheels": {"D": "1.100", "AIM": "4000", "Tw": "1.435", "Axle": "Bo'Bo'", "Ad": "2.6", "Bd": "10.1", "Rmin": "80"}, + "Brake": {"BrakeValve": "LSt", "NBpA": "4", "MBF": "85.0", "MaxBP": "3.8", "BCN": "4", "BCR": "0.0625", "BCD": "0.11", "BCM": "5.14", "HiPP": "5.0", "LoPP": "3.5", "Vv": "0.250", "MinCP": "7.5", "MaxCP": "8.5", "CompressorSpeed": "0.03", "BVV": "52", "BRE": "0.8", "BM": "P10-Bg", "BCS": "0.8", "BSA": "0"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "3"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "8", "SCPN": "3", "AutoRelay": "No", "IniCDelay": "0.5", "SCDelay": "0.1", "BrakeDelays": "GP", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "rows": [ + ["-1","0.8","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "110", "LMaxCurrent": "155"}, + "Power": {"EnginePower": "Internal"}, + "Engine": {"EngineType": "Dumb", "Ftmax": "250000"} +} diff --git a/tests/unit/data/fiz/wmb10_v1/wmb10.json b/tests/unit/data/fiz/wmb10_v1/wmb10.json new file mode 100644 index 000000000..8dc4272f6 --- /dev/null +++ b/tests/unit/data/fiz/wmb10_v1/wmb10.json @@ -0,0 +1,49 @@ +{ + "_source": "pkp/wmb10_v1/wmb10.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "9800", "Vmax": "70", "PWR": "92", "SandCap": "0", "Mred": "1000"}, + "Dimensions": {"L": "8.21", "H": "2.9", "W": "2.8", "Cx": "0.2"}, + "Load": {"MaxLoad": "6", "LoadQ": "pieces", "LoadAccepted": "Passengers,naprawcze,brygada_naprawcza,konserwacja", "OverLoadFactor": "1", "LoadSpeed": "0.5", "UnLoadSpeed": "0.5"}, + "Wheels": {"D": "0.7", "AIM": "2000", "Tw": "1.435", "Axle": "1'A", "Bd": "5.1", "Rmin": "80", "BearingType": "Roll"}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "Passenger", "OpenSpeed": "0.2", "CloseSpeed": "0.2", "DoorOpenMethod": "Rotate", "DoorMaxShiftL": "0.625", "DoorMaxShiftR": "0.625"}, + "Brake": {"BrakeValve": "LSt", "Size": "12", "NBpA": "1", "MBF": "11", "MaxBP": "4.0", "BCN": "2", "BCR": "0.1016", "BCD": "0.15", "BCM": "4.1", "BCS": "1.0", "BSA": "1.0", "BM": "Disk2", "BVV": "50", "BRE": "0.9", "MinCP": "6", "MaxCP": "7.3", "Vv": "0.2", "CompressorSpeed": "0.0125", "CompressorPower": "Main"}, + "SpringBrake": {"Volume": "9.4", "MBF": "11", "MaxSP": "5.5", "ResetP": "4.0", "MinFP": "3.5", "PressOff": "4.5", "PressOn": "4.2", "ValveOnArea": "0.0005", "ValveOffArea": "0.0005", "ValvePNBArea": "0.002", "MTC": "132"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.15", "FmaxB": "740", "kC": "4800", "DmaxC": "0.15", "FmaxC": "780", "beta": "0.58", "AllowedFlag": "3"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "BCPN": "6", "BDelay1": "15", "BDelay2": "4", "BDelay3": "35", "BDelay4": "25", "ASB": "Manual", "LocalBrake": "PneumaticBrake", "MCPN": "9", "SCPN": "5", "AutoRelay": "no", "IniCDelay": "0.48", "SCDelay": "0.3", "BrakeDelays": "P", "BrakeHandle": "FV4a", "LocBrakeHandle": "FD1", "FuelStart": "Automatic", "OilStart": "Automatic", "WaterStart": "Battery", "ConverterStart": "Automatic", "rows": [ + ["-1","0.8","-1","20","Pneumatic"], + ["0","0.5","-1","2","Pneumatic"], + ["1","0.46","-1","2","Pneumatic"], + ["2","0.42","-1","2","Pneumatic"], + ["3","0.38","-1","2","Pneumatic"], + ["4","0.34","-1","2","Pneumatic"], + ["5","0.29","-1","2","Pneumatic"], + ["6","0","-1","40","Pneumatic"] + ]}, + "Light": {"Light": "Generator", "LGeneratorEngine": "ElectricSeriesMotor", "LMaxVoltage": "24", "LMaxCurrent": "100"}, + "Engine": {"EngineType": "DieselEngine", "Trans": "10:54", "nmin": "575", "nmax": "2700", "nmax_cutoff": "2600", "AIM": "10", "EUS": "20", "EDS": "20"}, + "MotorParamTable": {"minVelfullengage": "1.62", "engageDia": "1", "engageMaxForce": "384", "engagefriction": "1", "rows": [ + ["0","0","0","-1"], + ["1","6.93","-1","10"], + ["2","4.03","4","17"], + ["3","2.36","9","30"], + ["4","1.40","18","53"], + ["5","1.00","33","100"] + ]}, + "DList": {"Size": "9", "Mmax": "450", "nMmax": "26.66", "Mnmax": "370", "nmax": "43.33", "nominalfill": "1", "Mstand": "130", "rows": [ + ["0","0.4","0"], + ["1","0.46","1"], + ["2","0.52","2"], + ["3","0.58","2"], + ["4","0.64","2"], + ["5","0.70","2"], + ["6","0.76","2"], + ["7","0.82","2"], + ["8","0.88","2"], + ["9","1.0","2"] + ]}, + "LightsList": {"Size": "3", "Wrap": "No", "Default": "2", "rows": [ + ["34","21"], + ["0","0"], + ["21","34"] + ]} +} diff --git a/tests/unit/data/fiz/z1_v2/152a.json b/tests/unit/data/fiz/z1_v2/152a.json new file mode 100644 index 000000000..8b33786a2 --- /dev/null +++ b/tests/unit/data/fiz/z1_v2/152a.json @@ -0,0 +1,14 @@ +{ + "_source": "pkp/z1_v2/152a.fiz", + "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads", + "Param": {"Category": "train", "M": "48000", "Vmax": "200", "HeatingP": "54", "LightP": "0.56"}, + "Load": {"MaxLoad": "80", "LoadQ": "pieces", "LoadAccepted": "Passengers,Luggage", "OverLoadFactor": "1.5", "LoadSpeed": "1", "UnLoadSpeed": "2"}, + "Dimensions": {"L": "26.4", "H": "4", "W": "2.9", "Cx": "0.01"}, + "Wheels": {"D": "0.92", "Tw": "1.435", "Axle": "2'2'", "Ad": "2.70", "Bd": "19", "Rmin": "100", "BearingType": "Roll"}, + "Brake": {"BrakeValve": "KE", "MBF": "11", "MaxBP": "3.8", "BCN": "12", "BCR": "0.127", "BCD": "0.05", "BCM": "1.42", "NBpA": "6", "RM": "1.4", "BM": "Disk1+Mg", "BCS": "1.2", "BSA": "0", "BVV": "130", "TBF": "32"}, + "BuffCoupl": {"CType": "Screw", "kB": "9200", "DmaxB": "0.11", "FmaxB": "320", "kC": "4500", "DmaxC": "0.1", "FmaxC": "850", "beta": "0.58", "AllowedFlag": "115"}, + "Cntrl": {"BrakeSystem": "Pneumatic", "LocalBrake": "ManualBrake", "BrakeDelays": "PR+Mg", "ASB": "Yes", "rows": []}, + "Light": {"Light": "Generator", "LGeneratorEngine": "WheelsDriven", "LMaxVoltage": "24", "AlterLight": "Accu", "AlterLMaxVoltage": "24", "AlterLCap": "495", "AlterLRS": "Generator"}, + "Clima": {"Heating": "PowerCable", "HPowerTrans": "ElectricPower", "HMaxVoltage": "3000"}, + "Doors": {"OpenCtrl": "Passenger", "CloseCtrl": "AutomaticCtrl", "OpenSpeed": "0.5", "CloseSpeed": "0.20", "DoorMaxShiftL": "0.9", "DoorMaxShiftR": "0.9", "DoorOpenMethod": "Plug", "DoorClosureWarning": "yes", "DoorBlocked": "Yes", "DoorMaxShiftPlug": "0.043"} +} diff --git a/tests/unit/data/golden/.gitignore b/tests/unit/data/golden/.gitignore new file mode 100644 index 000000000..b191ca629 --- /dev/null +++ b/tests/unit/data/golden/.gitignore @@ -0,0 +1,4 @@ +# the golden traces are recorded on the code before a change, not kept: the samples and their key figures +# (see tests/unit/README.md) +*.csv +*.figures diff --git a/tests/unit/drive.cpp b/tests/unit/drive.cpp new file mode 100644 index 000000000..0ee5adb2b --- /dev/null +++ b/tests/unit/drive.cpp @@ -0,0 +1,246 @@ +#include "stdafx.h" +#include "drive.h" +#include "consist.h" +#include "golden.h" +#include "doctest.h" + +#include "McZapkie/hamulce.h" + +namespace test { + +void mark(trace *Trace, std::string const &Event) { + if (Trace != nullptr) { + Trace->mark(Event); + } +} + +bool is_powered(TMoverParameters const &Vehicle) { + using enum TEngineType; + auto const type{Vehicle.EngineType}; + return Vehicle.Power > 1.0 && (type == ElectricSeriesMotor || type == ElectricInductionMotor || type == DieselEngine || type == DieselElectric); +} + +driven driving(vehicle &Vehicle) { + return {Vehicle.mover(), Vehicle.mover(), Vehicle.mover(), [&Vehicle](double Seconds, track const &Track) { Vehicle.run(Seconds, Track); }}; +} + +driven driving(consist &Consist) { + // the AI's mvControlling and mvPantographUnit: the first vehicle with an engine, and with pantographs + auto *controlling{&Consist[0].mover()}; + auto *pantographs{&Consist[0].mover()}; + for (auto i = Consist.size(); i-- > 0;) { + auto &mover{Consist[i].mover()}; + if (is_powered(mover)) { + controlling = &mover; + } + if (mover.EnginePowerSource.SourceType == TPowerSource::CurrentCollector && mover.EnginePowerSource.CollectorParameters.CollectorsNo > 0) { + pantographs = &mover; + } + } + return {Consist[0].mover(), *controlling, *pantographs, [&Consist](double Seconds, track const &Track) { Consist.run(Seconds, Track); }}; +} + +namespace { + +void attend(TMoverParameters &Mover) { + if (Mover.SecuritySystem.is_cabsignal_blinking() && Mover.SecuritySystem.has_separate_acknowledge()) { + Mover.SecuritySystem.cabsignal_reset(); + } + if (Mover.SecuritySystem.is_blinking()) { + Mover.SecuritySystemReset(); + } +} + +} // namespace + +void drive(driven Driven, track const &Track, double Seconds) { + auto const seconds{std::lround(Seconds)}; + for (long second = 0; second < seconds; ++second) { + attend(Driven.cab); + Driven.run(1.0, Track); + } +} + +void release_brakes(TMoverParameters &Cab) { + Cab.BrakeLevelSet(Cab.Handle->GetPos(bh_RP)); + Cab.DecLocalBrakeLevel(LocalBrakePosNo); + Cab.DecManualBrakeLevel(ManualBrakePosNo); + Cab.SpringBrakeActivate(false); + // the hand and spring brakes of the rest of the train, as TController::AutoRewident() releases them + std::vector train{&Cab}; + std::vector pending{&Cab}; + while (false == pending.empty()) { + auto const *vehicle{pending.back()}; + pending.pop_back(); + for (auto const &thiscoupler : vehicle->Couplers) { + if (thiscoupler.Connected != nullptr && thiscoupler.CouplingFlag != coupling::faux && std::ranges::find(train, thiscoupler.Connected) == train.end()) { + thiscoupler.Connected->DecManualBrakeLevel(ManualBrakePosNo); + thiscoupler.Connected->SpringBrake.Activate = false; + train.push_back(thiscoupler.Connected); + pending.push_back(thiscoupler.Connected); + } + } + } +} + +bool is_ready(driven const &Driven) { + auto const &mover{Driven.controlling}; + auto const air{mover.VeselVolume <= 0.0 || mover.ScndPipePress >= 4.5}; + // the converter feeds the 110 V circuits (the electrodynamic brake's control among them); the AI switches + // it on once the line breaker is closed, so a train already charged with air isn't ready before it runs + auto const converter{mover.ConverterStart != start_t::manual || mover.ConverterFlag}; + return mover.Mains && air && converter; +} + +namespace { + +// the pumps and the heater of a diesel's water, and the fuel pump +void prepare_diesel(driven const &Driven) { + Driven.cab.OilPumpSwitch(true); + Driven.controlling.WaterPumpBreakerSwitch(true); + Driven.controlling.WaterPumpSwitch(true); + Driven.controlling.WaterHeaterBreakerSwitch(true); + Driven.controlling.WaterHeaterSwitch(true); + Driven.cab.FuelPumpSwitch(true); +} + +// the pantographs' own air topped up by their compressor, and the pantographs raised +void raise_pantographs(driven const &Driven) { + auto &pantographs{Driven.pantographs}; + if (auto const enough{(pantographs.TrainType == dt_EZT ? 2.5 : 3.5) + 0.1}; pantographs.PantPress < enough) { + if (false == pantographs.PantAutoValve) { + pantographs.bPantKurek3 = false; // the pantographs on their own air + } + pantographs.PantCompFlag = true; + } + else if (false == pantographs.bPantKurek3 || pantographs.PantPress <= pantographs.ScndPipePress) { + pantographs.PantCompFlag = false; + } + Driven.cab.OperatePantographsValve(operation_t::enable); + Driven.cab.OperatePantographValve(end::rear, operation_t::enable); +} + +// the line breaker closed, which it does only with the controller at zero, and after the relays are reset +void close_line_breaker(driven const &Driven) { + auto &controlling{Driven.controlling}; + while (controlling.DecMainCtrl(2)) { + // down to zero + } + controlling.DecScndCtrl(2); + Driven.cab.RelayReset(relay_t::maincircuitground | relay_t::tractionnmotoroverload | relay_t::primaryconverteroverload); + if (controlling.EngineType == TEngineType::DieselEngine) { + // a mechanical diesel starts at idle, the first position giving the engine fuel + while (controlling.RList[controlling.MainCtrlPos].Mn == 0 && controlling.IncMainCtrl(1)) { + // up to the first position with fuel + } + } + Driven.cab.MainSwitch(true); +} + +// the converter, the compressor and the motor blowers, once the line breaker is closed +void switch_auxiliaries_on(TMoverParameters &Cab) { + Cab.ConverterSwitch(true); + Cab.CompressorSwitch(true); + for (auto const side : {end::front, end::rear}) { + if (Cab.MotorBlowers[side].speed != 0) { + Cab.MotorBlowersSwitchOff(false, side); + Cab.MotorBlowersSwitch(true, side); + } + } +} + +} // namespace + +bool start_up(driven Driven, track const &Track, double Seconds, trace *Trace) { + auto &cab{Driven.cab}; + auto const &controlling{Driven.controlling}; + auto const &pantographs{Driven.pantographs}; + mark(Trace, "start up"); + auto const diesel{controlling.EngineType == TEngineType::DieselEngine || controlling.EngineType == TEngineType::DieselElectric}; + auto const collector{pantographs.EnginePowerSource.SourceType == TPowerSource::CurrentCollector && + pantographs.EnginePowerSource.CollectorParameters.CollectorsNo > 0}; + // the converter and the compressor get switched on once the line breaker is closed, wherever in the train + // they are; a train already charged with air isn't ready before that + auto auxiliaries{false}; + // one round of the driver's actions a second, as the AI reacts + for (auto second = 0; second < Seconds; ++second) { + cab.BatterySwitch(true); + cab.CabActivisation(); + if (diesel) { + prepare_diesel(Driven); + } + if (collector) { + raise_pantographs(Driven); + } + if (false == collector || std::max(controlling.GetTrainsetHighVoltage(), controlling.PantographVoltage) > controlling.EnginePowerSource.CollectorParameters.MinV) { + // a direction first, as in TController::PrepareDirection(): the relays reset only with one set + for (int i = 0; i < 3 && cab.DirActive <= 0; ++i) { + cab.DirectionForward(); + } + if (false == controlling.Mains) { + close_line_breaker(Driven); + } + else { + switch_auxiliaries_on(cab); + auxiliaries = true; + } + } + drive(Driven, Track, 1.0); + if (auxiliaries && is_ready(Driven)) { + mark(Trace, "ready"); + return true; + } + } + return false; +} + +void drive_cycle(driven Driven, track const &Track, trace &Trace) { + auto &cab{Driven.cab}; + auto &controlling{Driven.controlling}; + if (false == start_up(Driven, Track, 300.0, &Trace)) { + MESSAGE("didn't get ready: Mains " << controlling.Mains << ", main reservoir " << controlling.ScndPipePress); + } + Trace.mark("release brakes"); + release_brakes(cab); + drive(Driven, Track, 30.0); + + Trace.mark("forward"); + for (int i = 0; i < 3 && cab.DirActive <= 0; ++i) { + cab.DirectionForward(); + } + drive(Driven, Track, 2.0); + + Trace.mark("notch up"); + for (int i = 0; i < 60 && false == controlling.IsMainCtrlMaxPowerPos(); ++i) { + controlling.IncMainCtrl(1); + drive(Driven, Track, 2.0); + } + Trace.mark("full power"); + drive(Driven, Track, 30.0); + + Trace.mark("second controller up"); + for (int i = 0; i < 20 && controlling.IncScndCtrl(1); ++i) { + drive(Driven, Track, 2.0); + } + drive(Driven, Track, 20.0); + Trace.mark("second controller down"); + for (int i = 0; i < 20 && controlling.DecScndCtrl(1); ++i) { + drive(Driven, Track, 1.0); + } + + Trace.mark("notch down"); + for (int i = 0; i < 60 && controlling.DecMainCtrl(1); ++i) { + drive(Driven, Track, 1.0); + } + Trace.mark("coast"); + drive(Driven, Track, 10.0); + + Trace.mark("brake"); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_FB)); + drive(Driven, Track, 60.0); + Trace.mark("release"); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_RP)); + drive(Driven, Track, 20.0); +} + +} // namespace test diff --git a/tests/unit/drive.h b/tests/unit/drive.h new file mode 100644 index 000000000..a1aed95be --- /dev/null +++ b/tests/unit/drive.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include "vehicle.h" + +namespace test { + +class trace; +class consist; + +// what a driver does with a vehicle, made of the steps the AI takes (TController and the driver hints of +// application/driverhints.cpp), on the vehicle's own controls. the driver sits in a vehicle with a cab +// (Cab != 0): a lone one, or the first one of a consist, and drives the whole of it + +// what the driver drives: the vehicle with the driver in it, the vehicles the driver controls the engine and +// the pantographs of (the AI's mvOccupied, mvControlling, mvPantographUnit; all the same one in a lone +// vehicle or a locomotive), and the way to run the whole of it +struct driven { + TMoverParameters &cab; + TMoverParameters &controlling; + TMoverParameters &pantographs; + std::function run; +}; +driven driving(vehicle &Vehicle); +driven driving(consist &Consist); + +// gets the vehicle ready to run, the way TController::PrepareEngine() does: battery, cab, pumps and heating +// of a diesel engine, pantographs, line breaker, converter, compressor and motor blowers, then waits for the +// air. stops trying after Seconds; returns whether the vehicle got ready +bool start_up(driven Driven, track const &Track, double Seconds = 300.0, trace *Trace = nullptr); + +// runs the vehicle for Seconds with the driver paying attention, as TController::control_security_system() +// does: once a second the vigilance and the cab signalling get acknowledged when they call for it +void drive(driven Driven, track const &Track, double Seconds); + +// releases every brake the driver has a hand on: the train brake to running, the independent brake, the +// hand brake and the spring brake; and the hand and spring brakes of the vehicles coupled to the cab's +void release_brakes(TMoverParameters &Cab); + +// whether the vehicle is ready to run: line breaker closed (or the engine running), air in the main reservoir +bool is_ready(driven const &Driven); + +// whether the vehicle drives itself +bool is_powered(TMoverParameters const &Vehicle); + +// a trace mark, when there's a trace +void mark(trace *Trace, std::string const &Event); + +// a whole run: start up, release the brakes, notch up to full power, the second controller (field weakening, +// shunting) up and down, notch down, coast and brake to a stop. the controllers move on a fixed schedule, +// not the way a driver watching the ammeter would, so the relays may trip on the way +void drive_cycle(driven Driven, track const &Track, trace &Trace); + +} // namespace test diff --git a/tests/unit/figures.cpp b/tests/unit/figures.cpp new file mode 100644 index 000000000..96f5a3d74 --- /dev/null +++ b/tests/unit/figures.cpp @@ -0,0 +1,222 @@ +#include "stdafx.h" +#include "figures.h" + +#include +#include +#include +#include + + +namespace test { + +namespace { + +// the units of the figures, with the decimals they're written with +struct unit { + char const *name; + int decimals; +}; +unit const seconds{"s", 2}; +unit const kmh{"km/h", 1}; +unit const metres{"m", 1}; +unit const bar{"bar", 3}; +unit const amperes{"A", 0}; +unit const kilonewtons{"kN", 1}; +std::array const units{seconds, kmh, metres, bar, amperes, kilonewtons}; + +std::string line(char const *Name, double Value, unit const &Unit) { + return std::format("{} = {:.{}f} {}", Name, Value, Unit.decimals, Unit.name); +} + +// the name, the value and the unit of a line of a figure, or false for the header of a part +bool parse(std::string_view const Line, std::string &Name, double &Value, std::string &Unit) { + auto const equals{Line.find(" = ")}; + if (equals == std::string_view::npos) { + return false; + } + Name = Line.substr(0, equals); + std::string const value{Line.substr(equals + 3)}; + auto const space{value.find(' ')}; + Value = std::strtod(value.substr(0, space).c_str(), nullptr); + Unit = space == std::string::npos ? std::string{} : value.substr(space + 1); + return true; +} + +// a difference of 0.5%, or of the last decimal written +bool close(double Recorded, double Now, std::string_view const Unit) { + auto step{0.0}; + for (auto const &unit : units) { + if (Unit == unit.name) { + step = std::pow(10.0, -unit.decimals); + } + } + return std::abs(Recorded - Now) <= 0.005 * std::max(std::abs(Recorded), std::abs(Now)) + step + 1e-9; +} + +// the drop of the brake pipe which the time to get back counts from, and the pressure it counts to +constexpr double pipe_drop{4.8}; +constexpr double pipe_back{4.9}; + +} // namespace + +void figures::sample(double Time, TMoverParameters const &Vehicle) { + auto &part{m_parts.back()}; + auto const speed{Vehicle.V * 3.6}; + auto const pipe{Vehicle.PipePress}; + if (false == part.sampled) { + part.sampled = true; + // from the last step of the part before, so the parts add up to the run + part.start = m_last_time < 0.0 ? Time : m_last_time; + part.speed_start = m_last_time < 0.0 ? speed : m_last_speed; + part.distance_start = m_last_time < 0.0 ? Vehicle.DistCounter : m_last_distance; + part.pipe_lowest = pipe; + } + part.end = Time; + part.speed_end = speed; + part.speed_peak = std::max(part.speed_peak, std::abs(speed)); + part.distance_end = Vehicle.DistCounter; + if (part.stopped < 0.0 && std::abs(part.speed_start) > 1.0 && std::abs(speed) < 0.05) { + part.stopped = Time - part.start; + } + if (pipe < part.pipe_lowest) { + part.pipe_lowest = pipe; + part.pipe_back = -1.0; + } + part.pipe_end = pipe; + if (part.pipe_back < 0.0 && part.pipe_lowest < pipe_drop && pipe >= pipe_back) { + part.pipe_back = Time - part.start; + } + part.cylinders_peak = std::max(part.cylinders_peak, Vehicle.BrakePress); + part.current_peak = std::max(part.current_peak, std::abs(Vehicle.Im)); + part.force_peak = std::max(part.force_peak, std::abs(Vehicle.Ft) / 1000.0); + + m_last_time = Time; + m_last_speed = speed; + m_last_distance = Vehicle.DistCounter; + // which figures the vehicle has, over the whole run, so each part has the same ones + m_moved |= std::abs(speed) >= 0.05; + m_piped |= pipe >= 0.1; + m_braked |= Vehicle.BrakePress >= 0.005; + m_current |= std::abs(Vehicle.Im) >= 0.5; + m_force |= std::abs(Vehicle.Ft) >= 50.0; +} + +void figures::mark(std::string const &Event) { + if (auto &part{m_parts.back()}; false == part.sampled) { + // nothing ran since the last event: a part of both + part.event = part.event == "start" ? Event : part.event + ", " + Event; + return; + } + m_parts.emplace_back(Event); +} + +std::vector figures::lines() const { + std::vector result; + for (auto const &part : m_parts) { + if (false == part.sampled) { + continue; + } + result.push_back("[" + part.event + "]"); + result.push_back(line("duration", part.end - part.start, seconds)); + if (m_moved) { + result.push_back(line("speed at the start", part.speed_start, kmh)); + result.push_back(line("speed at the end", part.speed_end, kmh)); + result.push_back(line("speed peak", part.speed_peak, kmh)); + result.push_back(line("distance", (part.distance_end - part.distance_start) * 1000.0, metres)); + if (part.stopped >= 0.0) { + result.push_back(line("stopped after", part.stopped, seconds)); + } + } + if (m_piped) { + result.push_back(line("brake pipe lowest", part.pipe_lowest, bar)); + result.push_back(line("brake pipe at the end", part.pipe_end, bar)); + if (part.pipe_back >= 0.0) { + result.push_back(line("brake pipe back at 4.9 bar after", part.pipe_back, seconds)); + } + } + if (m_braked) { + result.push_back(line("brake cylinders peak", part.cylinders_peak, bar)); + } + if (m_current) { + result.push_back(line("motor current peak", part.current_peak, amperes)); + } + if (m_force) { + result.push_back(line("tractive force peak", part.force_peak, kilonewtons)); + } + } + return result; +} + +namespace { + +struct figure { + std::string name; + double value; + std::string unit; + std::string text; // the value and the unit as written +}; + +struct recorded_part { + std::string header; + std::vector
figures; +}; + +std::vector parts_of(std::vector const &Lines) { + std::vector parts; + for (auto const &line : Lines) { + figure figure; + if (false == parse(line, figure.name, figure.value, figure.unit)) { + parts.emplace_back(line); + continue; + } + if (parts.empty()) { + parts.emplace_back(); + } + figure.text = line.substr(line.find(" = ") + 3); + parts.back().figures.push_back(figure); + } + return parts; +} + +figure const *find(std::vector
const &Figures, std::string_view const Name) { + for (auto const &figure : Figures) { + if (figure.name == Name) { + return &figure; + } + } + return nullptr; +} + +} // namespace + +std::vector figures::compare(std::vector const &Recorded, std::vector const &Now) { + std::vector differences; + auto const recorded{parts_of(Recorded)}; + auto const now{parts_of(Now)}; + for (std::size_t i = 0; i < std::max(recorded.size(), now.size()); ++i) { + if (i >= recorded.size() || i >= now.size() || recorded[i].header != now[i].header) { + // the parts can't be paired up from here on + differences.push_back(std::format("the parts differ from part {}: {} now, {} recorded", i + 1, i < now.size() ? now[i].header : std::string{"none"}, + i < recorded.size() ? recorded[i].header : std::string{"none"})); + break; + } + auto const &header{now[i].header}; + for (auto const &figure : recorded[i].figures) { + auto const *current{find(now[i].figures, figure.name)}; + if (current == nullptr) { + differences.push_back(header + " " + figure.name + ": none now, " + figure.text + " recorded"); + } + else if (current->unit != figure.unit || false == close(figure.value, current->value, figure.unit)) { + differences.push_back(header + " " + figure.name + ": " + current->text + " now, " + figure.text + " recorded"); + } + } + for (auto const &figure : now[i].figures) { + if (find(recorded[i].figures, figure.name) == nullptr) { + differences.push_back(header + " " + figure.name + ": " + figure.text + " now, none recorded"); + } + } + } + return differences; +} + +} // namespace test diff --git a/tests/unit/figures.h b/tests/unit/figures.h new file mode 100644 index 000000000..e69e67ef4 --- /dev/null +++ b/tests/unit/figures.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#include "McZapkie/MOVER.h" + +namespace test { + +// the key figures of a vehicle's run, for each part of it between two events of the test: the speeds, the +// distance, the time to stop, the brake pipe's lowest pressure and the time it took to get back, the brake +// cylinders', the motor current's and the tractive force's peaks. a golden trace keeps them next to its +// samples, in data/golden/.figures, which a person reads: +// +// [emergency] +// duration = 40.00 s +// speed at the start = 80.0 km/h +// speed at the end = 0.0 km/h +// distance = 412.3 m +// stopped after = 38.21 s +// brake cylinders peak = 3.800 bar +// +// a figure the vehicle doesn't have (no speed, no current) is left out of all the parts. they're compared +// within 0.5%, loose enough for any compiler, so a change which alters the behaviour on purpose shows in their +// diff in a few readable lines, where the samples change in thousands +class figures { +public: + // called on every step of the vehicle + void sample(double Time, TMoverParameters const &Vehicle); + // a part of the run starts with the event + void mark(std::string const &Event); + + // the figures as the lines of the file + std::vector lines() const; + // the differences of the lines from the recorded ones, beyond the tolerance, each told in a line + static std::vector compare(std::vector const &Recorded, std::vector const &Now); + +private: + struct part { + std::string event; + bool sampled{false}; + double start{0.0}; + double end{0.0}; + double speed_start{0.0}; + double speed_end{0.0}; + double speed_peak{0.0}; + double distance_start{0.0}; + double distance_end{0.0}; + double stopped{-1.0}; // after the start, if it started moving and stopped + double pipe_lowest{0.0}; + double pipe_end{0.0}; + double pipe_back{-1.0}; // after the start, at 4.9 bar again after a drop under 4.8 + double cylinders_peak{0.0}; + double current_peak{0.0}; + double force_peak{0.0}; + }; + + std::vector m_parts{part{"start"}}; + double m_last_time{-1.0}; + double m_last_speed{0.0}; + double m_last_distance{0.0}; + // the figures the vehicle has, shown in every part + bool m_moved{false}; + bool m_piped{false}; + bool m_braked{false}; + bool m_current{false}; + bool m_force{false}; +}; + +} // namespace test diff --git a/tests/unit/fiz.cpp b/tests/unit/fiz.cpp new file mode 100644 index 000000000..38310db46 --- /dev/null +++ b/tests/unit/fiz.cpp @@ -0,0 +1,222 @@ +#include "stdafx.h" +#include "fiz.h" + +#include +#include +#include + +#include "utilities/parser.h" + +namespace test::fiz { + +namespace { + +// the lists of LoadFIZ_Parse(), as bits of the set of the ones reading a row +constexpr unsigned brake_table{1 << 0}; // startBPT +constexpr unsigned motor_table{1 << 1}; // startMPT +constexpr unsigned motor_table0{1 << 2}; // startMPT0 +constexpr unsigned resistors{1 << 3}; // startRLIST +constexpr unsigned universal{1 << 4}; // startUCLIST +constexpr unsigned diesel{1 << 5}; // startDLIST +constexpr unsigned diesel_momentum{1 << 6}; // startDIZELMOMENTUMLIST +constexpr unsigned diesel_v2n{1 << 7}; // startDIZELV2NMAXLIST +constexpr unsigned hydro{1 << 8}; // startHYDROTCLIST +constexpr unsigned diesel_pmax{1 << 9}; // startPMAXLIST +constexpr unsigned field{1 << 10}; // startFFLIST +constexpr unsigned field_brake{1 << 11}; // startFFEDLIST +constexpr unsigned generator{1 << 12}; // startWWLIST +constexpr unsigned wipers{1 << 13}; // startWiperList +constexpr unsigned dimmer{1 << 14}; // startDimmerList +constexpr unsigned lights{1 << 15}; // startLIGHTSLIST +constexpr unsigned compressors{1 << 16}; // startCOMPRESSORLIST + +// the issection() calls of LoadFIZ_Parse() +std::array const sections{ + section{"Param", '.'}, + section{"Load", ':'}, + section{"Dimensions", ':'}, + section{"Wheels", ':'}, + section{"Brake", ':'}, + section{"Doors", ':'}, + section{"BuffCoupl", '.'}, + section{"BuffCoupl1", '.'}, + section{"BuffCoupl2", '.'}, + section{"TurboPos", ':'}, + section{"Cntrl", '.', brake_table, ""}, + section{"Headlights", ':'}, + section{"Blending", ':'}, + section{"DCEMUED", ':'}, + section{"SpringBrake", ':'}, + section{"Light", ':'}, + section{"Security", ':'}, + section{"Clima", ':'}, + section{"Power", ':'}, + section{"SpeedControl", ':'}, + section{"Engine", ':'}, + section{"Switches", ':'}, + section{"MotorParamTable", ':', motor_table, "END-MPT"}, + section{"MotorParamTable0", ':', motor_table0, "END-MPT"}, + section{"Circuit", ':'}, + section{"AI", ':'}, + section{"RList", ':', resistors, "END-RL"}, + section{"UCList", ':', universal, "END-UCL"}, + section{"DList", ':', diesel, "END-DL"}, + section{"DMList", ':', diesel_momentum, "END-DML"}, + section{"HTCList", ':', hydro, "END-HTCL"}, + section{"PmaxList", ':', diesel_pmax, "END-PML"}, + section{"V2NList", ':', diesel_v2n, "END-V2NL"}, + section{"ffList", ':', field, "endff"}, + section{"ffBrakeList", ':', field_brake, "endff"}, + section{"WWList", ':', generator, "END-WWL"}, + section{"WiperList", ':', wipers, "endwl"}, + section{"DimmerList", ':', dimmer, "endDimmerList"}, + section{"LightsList", ':', lights, "endL"}, + section{"CompressorList", ':', compressors, "endCL"}, +}; + +// the lines closing lists, checked by the loader before the sections in this order, each closing the brake table too +struct closing { + std::string_view line; + unsigned lists; +}; +std::array const closings{ + closing{"END-MPT", motor_table | motor_table0}, + closing{"END-RL", resistors}, + closing{"END-UCL", universal}, + closing{"END-DL", diesel}, + closing{"END-DML", diesel_momentum}, + closing{"END-V2NL", diesel_v2n}, + closing{"END-HTCL", hydro}, + closing{"END-PML", diesel_pmax}, + closing{"endff", field | field_brake}, + closing{"endwl", wipers}, + closing{"endDimmerList", dimmer}, + closing{"END-WWL", generator}, + closing{"endL", lights}, + closing{"endCL", compressors}, +}; + +} // namespace + +std::string value(std::string const &Key, std::string const &Line) { + std::string const input{" " + Line}; + auto const lookup{input.find(" " + Key + "=")}; + if (lookup == std::string::npos) { + return {}; + } + auto const start{input.find_first_not_of(' ', lookup + Key.size() + 2)}; + if (start == std::string::npos) { + return {}; + } + auto const end{input.find(' ', start)}; + return input.substr(start, end == std::string::npos ? std::string::npos : end - start); +} + +std::vector words(std::string_view const Line) { + std::vector result; + std::size_t start{0}; + while ((start = Line.find_first_not_of(" \t;", start)) != std::string_view::npos) { + auto const end{Line.find_first_of(" \t;", start)}; + result.emplace_back(Line.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start)); + start = end; + } + return result; +} + +section const *named(std::string_view Name) { + auto const lookup{std::ranges::find(sections, Name, §ion::name)}; + return lookup == sections.end() ? nullptr : std::to_address(lookup); +} + +section const *opened_by(std::string_view Line) { + auto const lookup{std::ranges::find_if(sections, [&](section const &Section) { return Line.starts_with(Section.name) && Line.size() > Section.name.size() && Line[Section.name.size()] == Section.separator; })}; + return lookup == sections.end() ? nullptr : std::to_address(lookup); +} + +std::vector lines(cParser &Parser) { + std::vector result; + while (Parser.ok()) { + auto line{Parser.getToken(false, "\n\r")}; + if (line.starts_with("//") || line.find('#') != std::string::npos) { + continue; + } + line.erase(0, line.find_first_not_of(' ')); + if (false == line.empty() && line.back() == '\r') { + line.pop_back(); + } + result.push_back(line); + } + return result; +} + +std::vector keys(std::string const &Line) { + std::vector result; + std::string const input{" " + Line}; + for (auto equals{input.find('=')}; equals != std::string::npos; equals = input.find('=', equals + 1)) { + auto const start{input.rfind(' ', equals) + 1}; + auto key{input.substr(start, equals - start)}; + // a=b=c gives a, not a=b + if (false == key.empty() && key.find('=') == std::string::npos && std::ranges::find(result, key) == result.end()) { + result.push_back(std::move(key)); + } + } + return result; +} + +// the values the section gives, as the loader asks for them by their keys +std::string section_values(section const &Section, std::string const &Line) { + auto keys{fiz::keys(Line)}; + std::ranges::sort(keys); + std::string values{Section.name}; + for (auto const &key : keys) { + if (auto const value{fiz::value(key, Line)}; false == value.empty()) { + values += " " + key + "=" + value; + } + } + return values; +} + +// the lists reading a row: the first one open, in the order of the loader's checks, which carries on past the dimmer list +unsigned row_readers(unsigned const Active) { + unsigned readers{0}; + for (unsigned list = brake_table; list <= compressors; list <<= 1) { + if ((Active & list) == 0) { + continue; + } + readers |= list; + if (list != dimmer) { + break; + } + } + return readers; +} + +std::vector reading(std::vector const &Lines) { + std::vector result; + unsigned active{0}; + for (auto const &line : Lines) { + if (line.empty()) { + active &= ~brake_table; + continue; + } + if (auto const close{std::ranges::find_if(closings, [&](closing const &Closing) { return line.starts_with(Closing.line); })}; close != closings.end()) { + active &= ~(close->lists | brake_table); + continue; + } + if (auto const opened{opened_by(line)}; opened != nullptr) { + active = (active & ~brake_table) | opened->list; + result.push_back(section_values(*opened, line)); + continue; + } + if (auto const readers{row_readers(active)}; readers != 0) { + std::string row{std::format("row of {:x}:", readers)}; + for (auto const &word : words(line)) { + row += " " + word; + } + result.push_back(std::move(row)); + } + } + return result; +} + +} // namespace test::fiz diff --git a/tests/unit/fiz.h b/tests/unit/fiz.h new file mode 100644 index 000000000..1e65049fc --- /dev/null +++ b/tests/unit/fiz.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +class cParser; + +// what the game's vehicle loader (TMoverParameters::LoadFIZ_Parse) makes of the lines of a vehicle definition, +// for the test definitions to give it the same and for checking they do +namespace test::fiz { + +// a section of the definition, opened by a line starting with its name and separator. a list section is +// followed by rows, read until the line closing them (an empty line for the Cntrl brake table) and as +// long as no other list closes first, see reading() +struct section { + std::string_view name; + char separator; + unsigned list{0}; // the bit of the list its rows go to, none for a section of values only + char const *end{nullptr}; // the line closing the rows +}; + +// the section of the given name, or none for a name the loader doesn't know +section const *named(std::string_view Name); +// the section the line opens, or none +section const *opened_by(std::string_view Line); + +// the lines of a definition as the loader takes them: without comments, leading spaces and line ends +std::vector lines(cParser &Parser); + +// the keys of the values in the line, in the order of their first appearance: the words followed by =, +// which extract_value() finds +std::vector keys(std::string const &Line); + +// the value of the key in the line, as extract_value() gives it, empty for none (where it would throw too) +std::string value(std::string const &Key, std::string const &Line); +// the words of a row, as the readers of the lists get them from cParser +std::vector words(std::string_view Line); + +// what the loader reads from the lines, in order: every section line with the values it gives, every row +// with the lists reading it. two definitions reading the same load the same vehicle +std::vector reading(std::vector const &Lines); + +} // namespace test::fiz diff --git a/tests/unit/golden.cpp b/tests/unit/golden.cpp new file mode 100644 index 000000000..45bd1c532 --- /dev/null +++ b/tests/unit/golden.cpp @@ -0,0 +1,393 @@ +#include "stdafx.h" +#include "golden.h" +#include "doctest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "McZapkie/hamulce.h" + +namespace test { + +namespace channels { + +std::vector motion() { + return { + {"V", [](auto const &M) { return M.V; }}, + {"DistCounter", [](auto const &M) { return M.DistCounter; }}, + {"AccS", [](auto const &M) { return M.AccS; }}, + {"nrot", [](auto const &M) { return M.nrot; }}, + {"Ft", [](auto const &M) { return M.Ft; }}, + {"Fb", [](auto const &M) { return M.Fb; }}, + {"FStand", [](auto const &M) { return M.FStand; }}, + {"SlippingWheels", [](auto const &M) { return M.SlippingWheels ? 1.0 : 0.0; }}, + }; +} + +std::vector traction() { + return { + {"Mains", [](auto const &M) { return M.Mains ? 1.0 : 0.0; }}, + {"DirActive", [](auto const &M) { return M.DirActive; }}, + {"MainCtrlPos", [](auto const &M) { return M.MainCtrlPos; }}, + {"MainCtrlActualPos", [](auto const &M) { return M.MainCtrlActualPos; }}, + {"ScndCtrlPos", [](auto const &M) { return M.ScndCtrlPos; }}, + {"ScndCtrlActualPos", [](auto const &M) { return M.ScndCtrlActualPos; }}, + {"Im", [](auto const &M) { return M.Im; }}, + {"Itot", [](auto const &M) { return M.Itot; }}, + {"EngineVoltage", [](auto const &M) { return M.EngineVoltage; }}, + {"EnginePower", [](auto const &M) { return M.EnginePower; }}, + {"enrot", [](auto const &M) { return M.enrot; }}, + {"dizel_fill", [](auto const &M) { return M.dizel_fill; }}, + {"eimv_pr", [](auto const &M) { return M.eimv_pr; }}, + {"DynamicBrakeFlag", [](auto const &M) { return M.DynamicBrakeFlag ? 1.0 : 0.0; }}, + }; +} + +std::vector brakes() { + return { + {"BrakeCtrlPos", [](auto const &M) { return M.BrakeCtrlPos; }}, + {"PipePress", [](auto const &M) { return M.PipePress; }}, + {"BrakePress", [](auto const &M) { return M.BrakePress; }}, + {"LocBrakePress", [](auto const &M) { return M.LocBrakePress; }}, + {"BRP", [](auto const &M) { return M.Hamulec->GetBRP(); }}, + {"VRP", [](auto const &M) { return M.Hamulec->GetVRP(); }}, + {"CRP", [](auto const &M) { return M.Hamulec->GetCRP(); }}, + }; +} + +std::vector air() { + return { + {"Compressor", [](auto const &M) { return M.Compressor; }}, + {"CompressorFlag", [](auto const &M) { return M.CompressorFlag ? 1.0 : 0.0; }}, + {"ScndPipePress", [](auto const &M) { return M.ScndPipePress; }}, + }; +} + +std::vector handles() { + std::vector result{ + {"BrakeCtrlPosR", [](auto const &M) { return M.BrakeCtrlPosR; }}, + {"dpMainValve", [](auto const &M) { return M.dpMainValve; }}, + {"HandleCP", [](auto const &M) { return M.Handle->GetCP(); }}, + {"HandleRP", [](auto const &M) { return M.Handle->GetRP(); }}, + {"HandleEP", [](auto const &M) { return M.Handle->GetEP(); }}, + {"LocalBrakePosA", [](auto const &M) { return M.LocalBrakePosA; }}, + {"dpLocalValve", [](auto const &M) { return M.dpLocalValve; }}, + }; + for (int i = 0; i < 5; ++i) { + result.emplace_back(std::format("HandleSound{}", i), [i](auto const &M) { return M.Handle->GetSound(i); }); + } + return result; +} + +std::vector doors() { + std::vector result{ + {"DoorsLocked", [](auto const &M) { return M.Doors.is_locked ? 1.0 : 0.0; }}, + }; + for (int side : {side::right, side::left}) { + auto const name{std::string{side == side::right ? "Right" : "Left"}}; + result.emplace_back(name + "Position", [side](auto const &M) { return M.Doors.instances[side].position; }); + result.emplace_back(name + "Step", [side](auto const &M) { return M.Doors.instances[side].step_position; }); + result.emplace_back(name + "Open", [side](auto const &M) { return M.Doors.instances[side].is_open ? 1.0 : 0.0; }); + result.emplace_back(name + "Closed", [side](auto const &M) { return M.Doors.instances[side].is_closed ? 1.0 : 0.0; }); + result.emplace_back(name + "Permit", [side](auto const &M) { return M.Doors.instances[side].open_permit ? 1.0 : 0.0; }); + } + return result; +} + +std::vector parameters() { + std::vector result{ + {"CategoryFlag", [](auto const &M) { return M.CategoryFlag; }}, + {"TrainType", [](auto const &M) { return M.TrainType; }}, + {"EngineType", [](auto const &M) { return static_cast(M.EngineType); }}, + {"Vmax", [](auto const &M) { return M.Vmax; }}, + {"Mass", [](auto const &M) { return M.Mass; }}, + {"TotalMass", [](auto const &M) { return M.TotalMass; }}, + {"Mred", [](auto const &M) { return M.Mred; }}, + {"Power", [](auto const &M) { return M.Power; }}, + {"HeatingPower", [](auto const &M) { return M.HeatingPower; }}, + {"LightPower", [](auto const &M) { return M.LightPower; }}, + {"BatteryVoltage", [](auto const &M) { return M.BatteryVoltage; }}, + {"Cx", [](auto const &M) { return M.Cx; }}, + {"WheelDiameter", [](auto const &M) { return M.WheelDiameter; }}, + {"TrackW", [](auto const &M) { return M.TrackW; }}, + {"AxleInertialMoment", [](auto const &M) { return M.AxleInertialMoment; }}, + {"NPoweredAxles", [](auto const &M) { return M.NPoweredAxles; }}, + {"NAxles", [](auto const &M) { return M.NAxles; }}, + {"BearingType", [](auto const &M) { return M.BearingType; }}, + {"ADist", [](auto const &M) { return M.ADist; }}, + {"BDist", [](auto const &M) { return M.BDist; }}, + {"NBpA", [](auto const &M) { return M.NBpA; }}, + {"SandCapacity", [](auto const &M) { return M.SandCapacity; }}, + {"BrakeSystem", [](auto const &M) { return static_cast(M.BrakeSystem); }}, + {"BrakeValve", [](auto const &M) { return static_cast(M.BrakeValve); }}, + {"BrakeHandle", [](auto const &M) { return static_cast(M.BrakeHandle); }}, + {"BrakeLocHandle", [](auto const &M) { return static_cast(M.BrakeLocHandle); }}, + {"LocalBrake", [](auto const &M) { return static_cast(M.LocalBrake); }}, + {"ASBType", [](auto const &M) { return M.ASBType; }}, + {"MaxBrakeForce", [](auto const &M) { return M.MaxBrakeForce; }}, + {"P2FTrans", [](auto const &M) { return M.P2FTrans; }}, + {"BrakeMethod", [](auto const &M) { return M.BrakeMethod; }}, + {"HighPipePress", [](auto const &M) { return M.HighPipePress; }}, + {"LowPipePress", [](auto const &M) { return M.LowPipePress; }}, + {"DeltaPipePress", [](auto const &M) { return M.DeltaPipePress; }}, + {"CntrlPipePress", [](auto const &M) { return M.CntrlPipePress; }}, + {"BrakeVolume", [](auto const &M) { return M.BrakeVolume; }}, + {"BrakeVVolume", [](auto const &M) { return M.BrakeVVolume; }}, + {"VeselVolume", [](auto const &M) { return M.VeselVolume; }}, + {"BrakeCylNo", [](auto const &M) { return M.BrakeCylNo; }}, + {"BrakeCylRadius", [](auto const &M) { return M.BrakeCylRadius; }}, + {"BrakeCylDist", [](auto const &M) { return M.BrakeCylDist; }}, + {"BrakeCylSpring", [](auto const &M) { return M.BrakeCylSpring; }}, + {"BrakeSlckAdj", [](auto const &M) { return M.BrakeSlckAdj; }}, + {"BrakeRigEff", [](auto const &M) { return M.BrakeRigEff; }}, + {"RapidMult", [](auto const &M) { return M.RapidMult; }}, + {"RapidVel", [](auto const &M) { return M.RapidVel; }}, + {"BrakeValveSize", [](auto const &M) { return M.BrakeValveSize; }}, + {"MinCompressor", [](auto const &M) { return M.MinCompressor; }}, + {"MaxCompressor", [](auto const &M) { return M.MaxCompressor; }}, + {"CompressorSpeed", [](auto const &M) { return M.CompressorSpeed; }}, + {"EmergencyValveOn", [](auto const &M) { return M.EmergencyValveOn; }}, + {"AirLeakRate", [](auto const &M) { return M.AirLeakRate; }}, + {"BrakeCtrlPosNo", [](auto const &M) { return M.BrakeCtrlPosNo; }}, + {"MainCtrlPosNo", [](auto const &M) { return M.MainCtrlPosNo; }}, + {"ScndCtrlPosNo", [](auto const &M) { return M.ScndCtrlPosNo; }}, + {"LightsPosNo", [](auto const &M) { return M.LightsPosNo; }}, + {"MBrake", [](auto const &M) { return M.MBrake ? 1.0 : 0.0; }}, + {"StopBrakeDecc", [](auto const &M) { return M.StopBrakeDecc; }}, + {"BrakeDelays", [](auto const &M) { return M.BrakeDelays; }}, + {"BrakeOpModes", [](auto const &M) { return M.BrakeOpModes; }}, + {"Imin", [](auto const &M) { return M.Imin; }}, + {"Imax", [](auto const &M) { return M.Imax; }}, + {"EIMCtrlType", [](auto const &M) { return M.EIMCtrlType; }}, + {"Volume", [](auto const &M) { return M.Volume; }}, + {"PantVolume", [](auto const &M) { return M.PantVolume; }}, + }; + for (int i = 0; i < 5; ++i) { + result.emplace_back(std::format("MaxBrakePress{}", i), [i](auto const &M) { return M.MaxBrakePress[i]; }); + } + for (int i = 0; i < 3; ++i) { + result.emplace_back(std::format("BrakeCylMult{}", i), [i](auto const &M) { return M.BrakeCylMult[i]; }); + } + return result; +} + +std::vector operator+(std::vector Left, std::vector const &Right) { + Left.insert(Left.end(), Right.begin(), Right.end()); + return Left; +} + +} // namespace channels + +namespace { + +bool environment(char const *Name) { + auto const *value{std::getenv(Name)}; + return value != nullptr && value[0] != '\0' && value[0] != '0'; +} + +std::string number(double Value) { + // the shortest text which reads back as the same double + std::array text{}; + auto const result{std::to_chars(text.data(), text.data() + text.size(), Value)}; + return {text.data(), result.ptr}; +} + +// a line of the file, whichever line ends the checkout gave it +bool read_line(std::ifstream &File, std::string &Line) { + if (false == static_cast(std::getline(File, Line))) { + return false; + } + if (false == Line.empty() && Line.back() == '\r') { + Line.pop_back(); + } + return true; +} + +std::vector split(std::string const &Line) { + std::vector fields; + std::stringstream stream(Line); + std::string field; + while (std::getline(stream, field, ',')) { + fields.push_back(field); + } + if (false == Line.empty() && Line.back() == ',') { + fields.emplace_back(); + } + return fields; +} + +bool same(std::string const &Recorded, std::string const &Now, bool const Exact) { + if (Recorded == Now) { + return true; + } + if (Exact) { + return false; + } + char *end{nullptr}; + auto const recorded{std::strtod(Recorded.c_str(), &end)}; + auto const now{std::strtod(Now.c_str(), &end)}; + if (std::isnan(recorded) || std::isnan(now)) { + return std::isnan(recorded) && std::isnan(now); + } + return std::abs(recorded - now) <= 1e-9 * std::max(std::abs(recorded), std::abs(now)) + 1e-12; +} + +} // namespace + +trace::trace(std::string Name, std::vector Channels, int Every, bool Figures) + : m_name(std::move(Name)), m_channels(std::move(Channels)), m_every(Every), m_keep_figures(Figures) {} + +void trace::sample(double Time, TMoverParameters const &Vehicle) { + // the invariants on every step, the first time each is broken + for (auto const &invariant : invariants()) { + if (std::ranges::find(m_broken, invariant.name) != m_broken.end()) { + continue; + } + if (false == invariant.holds(Vehicle, Time, m_sampled ? &m_before : nullptr)) { + m_broken.push_back(invariant.name); + m_violations.push_back(std::format("invariant \"{}\" broken in {} at {:.3f} s{}: {}", invariant.name, m_name, Time, + m_last_event.empty() ? std::string{} : std::format(", after \"{}\"", m_last_event), invariant.explain(Vehicle))); + } + } + m_before = {Time, Vehicle.V, Vehicle.DistCounter}; + m_sampled = true; + m_figures.sample(Time, Vehicle); + + if (m_step++ % m_every != 0) { + return; + } + // the time is a label, summed up from the steps: milliseconds are all it needs + auto row{std::format("{:.3f}", Time)}; + for (auto const &channel : m_channels) { + row += "," + number(channel.value(Vehicle)); + } + row += "," + m_event; + m_event.clear(); + m_rows.push_back(row); +} + +void trace::mark(std::string const &Event) { + m_event += (m_event.empty() ? "" : " ") + Event; + m_last_event = Event; + m_figures.mark(Event); +} + +void trace::check_figures(std::filesystem::path const &Path, bool const Exact) const { + // like the samples, the key figures are recorded on the code before a change and compared after it + std::ifstream file(Path, std::ios::binary); + if (false == file.is_open()) { + if (Exact) { + FAIL_CHECK("no key figures " << Path.string() << ", run the tests with EU07_UNIT_TESTS_UPDATE_GOLDEN=1 on the code before the change to record them"); + } + return; + } + std::vector recorded; + std::string line; + while (read_line(file, line)) { + recorded.push_back(line); + } + auto const differences{figures::compare(recorded, m_figures.lines())}; + if (false == differences.empty()) { + std::string text; + for (auto const &difference : differences) { + text += "\n " + difference; + } + FAIL_CHECK("key figures of " << m_name << " differ:" << text); + } +} + +void trace::check() const { + for (std::size_t i = 0; i < m_violations.size(); ++i) { + if (false == excused(m_name, m_broken[i])) { + FAIL_CHECK(m_violations[i]); + } + } + + std::string header{"time"}; + for (auto const &channel : m_channels) { + header += "," + channel.name; + } + header += ",event"; + + // no spaces in the file names: the build depends on the files, and make doesn't take to them + auto name{m_name}; + std::ranges::replace(name, ' ', '_'); + std::filesystem::path const filename{name + ".csv"}; + auto const path{std::filesystem::path{EU07_UNIT_TESTS_DATA} / "golden" / filename}; + auto figurespath{path}; + figurespath.replace_extension(".figures"); + if (environment("EU07_UNIT_TESTS_UPDATE_GOLDEN")) { + record(path, figurespath, header); + return; + } + + // the samples aren't kept in the repository, they're recorded on the code before a change and compared after it + auto const exact{environment("EU07_UNIT_TESTS_GOLDEN_EXACT")}; + if (m_keep_figures) { + check_figures(figurespath, exact); + } + check_samples(path, header, exact); +} + +void trace::record(std::filesystem::path const &Path, std::filesystem::path const &FiguresPath, std::string const &Header) const { + std::filesystem::create_directories(Path.parent_path()); + std::ofstream file(Path, std::ios::binary); + file << Header << "\n"; + for (auto const &row : m_rows) { + file << row << "\n"; + } + if (m_keep_figures) { + std::ofstream figuresfile(FiguresPath, std::ios::binary); + for (auto const &line : m_figures.lines()) { + figuresfile << line << "\n"; + } + } + MESSAGE("recorded the golden trace " << Path.string()); +} + +void trace::check_samples(std::filesystem::path const &path, std::string const &header, bool const exact) const { + std::ifstream file(path, std::ios::binary); + if (false == file.is_open()) { + if (exact) { + FAIL_CHECK("no golden trace " << path.string() << ", run the tests with EU07_UNIT_TESTS_UPDATE_GOLDEN=1 on the code before the change to record it"); + } + return; + } + std::string line; + read_line(file, line); + if (line != header) { + FAIL_CHECK("golden trace " << m_name << " has other channels: " << line << ", the test records " << header); + return; + } + auto const names{split(header)}; + std::size_t row{0}; + while (read_line(file, line)) { + if (row >= m_rows.size()) { + FAIL_CHECK("golden trace " << m_name << " is longer: " << m_rows.size() << " rows now, more recorded"); + return; + } + auto const recorded{split(line)}; + auto const now{split(m_rows[row])}; + for (std::size_t field = 0; field < names.size(); ++field) { + auto const recordedvalue{field < recorded.size() ? recorded[field] : std::string{}}; + auto const nowvalue{field < now.size() ? now[field] : std::string{}}; + if (false == same(recordedvalue, nowvalue, exact || names[field] == "event")) { + FAIL_CHECK("golden trace " << m_name << " differs first at time " << now.front() << " in " << names[field] << ": " << nowvalue + << " now, " << recordedvalue << " recorded\n now: " << m_rows[row] << "\n recorded: " << line); + return; + } + } + ++row; + } + if (row != m_rows.size()) { + FAIL_CHECK("golden trace " << m_name << " is shorter: " << m_rows.size() << " rows now, " << row << " recorded"); + } +} + +} // namespace test diff --git a/tests/unit/golden.h b/tests/unit/golden.h new file mode 100644 index 000000000..cb82ed0da --- /dev/null +++ b/tests/unit/golden.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include + +#include "McZapkie/MOVER.h" +#include "figures.h" +#include "invariants.h" + +namespace test { + +// one value of the vehicle a trace follows +struct channel { + std::string name; + std::function value; +}; + +// sets of channels for the usual subjects of a test, which can be put together +namespace channels { +std::vector motion(); // speed, distance, forces +std::vector traction(); // controllers, current, voltage, engine +std::vector brakes(); // pressures of the brake pipe, cylinders and reservoirs +std::vector air(); // compressor and main reservoir +std::vector handles(); // the driver's brake handle and the local brake handle, with their sounds +std::vector doors(); // both sides' doors and steps, their permits and the lock +std::vector parameters(); // what the vehicle definition sets, as the loader read it +std::vector operator+(std::vector Left, std::vector const &Right); +} // namespace channels + +// characterisation of the simulation: the values of the channels over a run of a vehicle, compared with the +// ones the code gave when the trace got recorded, in tests/unit/data/golden/.csv (spaces as underscores). a difference fails the +// test, naming the channel and the time it shows up first. the samples are tens of megabytes and only mean +// something against the code before a change, so they stay out of the repository: running the tests with +// EU07_UNIT_TESTS_UPDATE_GOLDEN=1 records them on the code before the change, and after it they're compared. +// a trace with nothing recorded checks only the invariants and its test's own CHECKs, as the CI does. +// +// values are compared within a relative tolerance of 1e-9 (the CI builds with GCC and glibc, whose maths +// functions can differ from MSVC's in the last bits), or exactly with EU07_UNIT_TESTS_GOLDEN_EXACT=1, which is the check to +// run for a change which shouldn't alter anything at all, and which fails a trace with nothing recorded. +// +// besides the samples, a trace records the key figures of the run (figures.h) in .figures, the few +// lines a person reads of what a change altered, and checks the invariants (invariants.h) on every step +// +// test::trace trace{"eu07 notch up", test::channels::motion() + test::channels::traction()}; +// eu07.record(trace); +// ... run the vehicle ... +// trace.check(); +class trace { +public: + // samples every Every-th step of the vehicle: 100, a second at the usual step, is plenty for a run of minutes, + // as a difference stays once the simulation has taken it in + // Figures: whether to keep the key figures, which a trace sampled by hand, at times of its own, has no use for + trace(std::string Name, std::vector Channels, int Every = 10, bool Figures = true); + + // called by the vehicle after every step + void sample(double Time, TMoverParameters const &Vehicle); + // adds an event of the test to the trace, so a difference can be told apart from the moment of the action + void mark(std::string const &Event); + + // compares the trace and its key figures (figures.h) with the recorded ones, or records them, and reports the + // invariants broken + void check() const; + // the invariants broken in the trace (see invariants.h), the first time each, excused or not + std::vector const &violations() const { return m_violations; } + +private: + void check_figures(std::filesystem::path const &Path, bool Exact) const; + // writes the samples and the key figures, as recorded on the code before a change + void record(std::filesystem::path const &Path, std::filesystem::path const &FiguresPath, std::string const &Header) const; + // compares the samples with the recorded ones + void check_samples(std::filesystem::path const &path, std::string const &header, bool exact) const; + + std::string m_name; + std::vector m_channels; + int m_every; + long m_step{0}; + std::vector m_rows; // formatted, as in the file + std::string m_event; // marked since the last row + std::string m_last_event; // the last one marked, for the invariants broken after it + snapshot m_before{}; + bool m_sampled{false}; + std::vector m_broken; // the names of the invariants broken so far + std::vector m_violations; + bool m_keep_figures; + figures m_figures; +}; + +} // namespace test diff --git a/tests/unit/invariants.cpp b/tests/unit/invariants.cpp new file mode 100644 index 000000000..4fafeb04d --- /dev/null +++ b/tests/unit/invariants.cpp @@ -0,0 +1,163 @@ +#include "stdafx.h" +#include "invariants.h" + +#include +#include +#include +#include + +namespace test { + +namespace { + +std::string values(std::initializer_list> Values) { + std::string result; + for (auto const &[name, value] : Values) { + result += std::format("{}{}={:.6g}", result.empty() ? "" : " ", name, value); + } + return result; +} + +bool electric(TMoverParameters const &M) { + return M.EngineType == TEngineType::ElectricSeriesMotor || M.EngineType == TEngineType::ElectricInductionMotor; +} + +double max_brake_press(TMoverParameters const &M) { + auto result{0.0}; + for (auto const press : M.MaxBrakePress) { + result = std::max(result, press); + } + return result; +} + +// the margin over a limit which a step of the simulation may overshoot it by +constexpr double pressure_margin{0.05}; +// the highest acceleration of a vehicle, 5 g, which a collision goes well over +constexpr double highest_acceleration{5.0 * 9.81}; + +} // namespace + +std::vector const &invariants() { + static std::vector const rules{ + {"every value a number", + [](auto const &M, double, auto const *) { + std::initializer_list const values{M.V, M.AccS, M.Ft, M.Fb, M.Im, M.Itot, M.EngineVoltage, + M.enrot, M.PipePress, M.BrakePress, M.LocBrakePress, M.ScndPipePress, M.Compressor, M.BatteryVoltage}; + return std::ranges::all_of(values, [](double const Value) { return std::isfinite(Value); }); + }, + [](auto const &M) { + return values({{"V", M.V}, {"AccS", M.AccS}, {"Ft", M.Ft}, {"Fb", M.Fb}, {"Im", M.Im}, {"Itot", M.Itot}, {"EngineVoltage", M.EngineVoltage}, + {"enrot", M.enrot}, {"PipePress", M.PipePress}, {"BrakePress", M.BrakePress}, {"LocBrakePress", M.LocBrakePress}, + {"ScndPipePress", M.ScndPipePress}, {"Compressor", M.Compressor}, {"BatteryVoltage", M.BatteryVoltage}}); + }}, + {"brake cylinders between empty and the vehicle's highest pressure", + [](auto const &M, double, auto const *) { + // a step overshoots the setting a little: the EU07's LSt fills its cylinders to 4.0 bar of its 3.8 + auto const limit{1.1 * max_brake_press(M) + pressure_margin}; + return M.BrakePress >= -pressure_margin && M.BrakePress <= limit && M.LocBrakePress >= -pressure_margin && M.LocBrakePress <= limit; + }, + [](auto const &M) { return values({{"BrakePress", M.BrakePress}, {"LocBrakePress", M.LocBrakePress}, {"max(MaxBrakePress)", max_brake_press(M)}}); }}, + {"pipes between empty and the highest governor setting", + [](auto const &M, double, auto const *) { + // all the air comes from the main reservoirs, which the game sets up to 10 bar, some (the Impuls) to 12; + // the driver's valve in its filling position joins the brake pipe to them + auto const highest{1.01 * std::max(10.0, M.MaxCompressor) + pressure_margin}; + std::initializer_list const pressures{M.PipePress, M.ScndPipePress, M.Compressor}; + return std::ranges::all_of(pressures, [highest](double const Press) { return Press >= -pressure_margin && Press <= highest; }); + }, + [](auto const &M) { return values({{"PipePress", M.PipePress}, {"ScndPipePress", M.ScndPipePress}, {"Compressor", M.Compressor}}); }}, + {"main reservoir no higher than the compressor's governor", + [](auto const &M, double, auto const *) { return M.MaxCompressor <= 0.0 || M.Compressor <= 1.01 * M.MaxCompressor + pressure_margin; }, + [](auto const &M) { return values({{"Compressor", M.Compressor}, {"MaxCompressor", M.MaxCompressor}}); }}, + {"no motor current with the line breaker open", + [](auto const &M, double, auto const *) { return M.Mains || false == electric(M) || std::abs(M.Im) < 1.0; }, + [](auto const &M) { return values({{"Mains", M.Mains}, {"Im", M.Im}, {"Itot", M.Itot}}); }}, + {"no motor current with the line contactors open, but for the dynamic brake", + [](auto const &M, double, auto const *) { + // the dynamic brake closes its own circuit of the motors and the resistors + return M.StLinFlag || M.DynamicBrakeFlag || M.EngineType != TEngineType::ElectricSeriesMotor || std::abs(M.Im) < 1.0; + }, + [](auto const &M) { + return values({{"StLinFlag", M.StLinFlag}, {"DynamicBrakeFlag", M.DynamicBrakeFlag}, {"Im", M.Im}, {"MainCtrlActualPos", M.MainCtrlActualPos}}); + }}, + {"no tractive force without power", + [](auto const &M, double, auto const *) { return M.Power >= 0.01 || std::abs(M.Ft) < 1.0; }, + [](auto const &M) { return values({{"Power", M.Power}, {"Ft", M.Ft}}); }}, + {"engine fill between none and full", + [](auto const &M, double, auto const *) { return M.dizel_fill >= 0.0 && M.dizel_fill <= 1.0; }, + [](auto const &M) { return values({{"dizel_fill", M.dizel_fill}}); }}, + {"diesel engine turning forward, no faster than its top speed", + [](auto const &M, double, auto const *) { + // the governor lets it overshoot a little (the SN61 by 1%) + return M.EngineType != TEngineType::DieselEngine || (M.enrot >= 0.0 && M.enrot <= 1.05 * M.dizel_nmax); + }, + [](auto const &M) { return values({{"enrot", M.enrot}, {"dizel_nmax", M.dizel_nmax}}); }}, + {"battery no higher than its nominal voltage", + [](auto const &M, double, auto const *) { return M.NominalBatteryVoltage <= 0.0 || M.BatteryVoltage <= 1.05 * M.NominalBatteryVoltage; }, + [](auto const &M) { return values({{"BatteryVoltage", M.BatteryVoltage}, {"NominalBatteryVoltage", M.NominalBatteryVoltage}}); }}, + {"distance counted as the speed runs", + [](auto const &M, double Time, auto const *Before) { + if (Before == nullptr) { + return true; + } + // the speed of the step is between the speeds at its ends, or a bit off it, as the movement is integrated; + // through a standstill, down to none + auto const dt{Time - Before->time}; + auto const covered{(M.DistCounter - Before->DistCounter) * 1000.0}; + auto const lowest{M.V * Before->V > 0.0 ? std::min(std::abs(M.V), std::abs(Before->V)) : 0.0}; + auto const highest{std::max(std::abs(M.V), std::abs(Before->V))}; + return covered >= lowest * dt * 0.9 - 0.01 && covered <= highest * dt * 1.1 + 0.01; + }, + [](auto const &M) { return values({{"V", M.V}, {"DistCounter", M.DistCounter}}); }}, + {"no acceleration over 5 g", + [](auto const &M, double Time, auto const *Before) { + return Before == nullptr || std::abs(M.V - Before->V) <= highest_acceleration * (Time - Before->time) + 0.01; + }, + [](auto const &M) { return values({{"V", M.V}, {"AccS", M.AccS}}); }}, + {"hand brake applied, the wheels braked on the move", + [](auto const &M, double, auto const *) { return false == M.MBrake || M.ManualBrakePos == 0 || std::abs(M.V) < 0.5 || std::abs(M.Fb) > 0.0; }, + [](auto const &M) { return values({{"ManualBrakePos", M.ManualBrakePos}, {"V", M.V}, {"Fb", M.Fb}}); }}, + }; + return rules; +} + +namespace { + +struct excuse { + char const *trace; // a part of the trace's name + char const *invariant; + char const *reason; +}; + +std::vector const excuses{ + // what the tests do + {"couplers/collision/", "no acceleration over 5 g", "vehicles run into each other"}, + {"controls/collision/", "no acceleration over 5 g", "vehicles run into each other"}, + {"couplers/collision/", "distance counted as the speed runs", "the buffers throw the vehicle back within a step"}, + {"switches_live/", "no acceleration over 5 g", "sampled by hand, a second apart in the trace for 2 s run"}, + {"switches_live/", "distance counted as the speed runs", "sampled by hand, a second apart in the trace for 2 s run"}, + {"brake handles/", "main reservoir no higher than the compressor's governor", "the test holds the main reservoir at 8 bar"}, + {"local brake/", "main reservoir no higher than the compressor's governor", "the test holds the main reservoir at 8 bar"}, + {"brake cycle/at speed/", "brake cylinders between empty and the vehicle's highest pressure", "the rapid step of the valves at speed"}, + // the vehicles as they are + {"elf/2", "main reservoir no higher than the compressor's governor", + "the units' reservoirs share the supply pipe: the A car's governor stops at 9 bar, the C car's at 8.5"}, + {"sm03_v1/", "brake cylinders between empty and the vehicle's highest pressure", + "its K valve fills the cylinders to the auxiliary reservoir's pressure (4.27 bar), over MaxBP 3.5"}, + {"eu04_v1/", "brake cylinders between empty and the vehicle's highest pressure", + "its K valve fills the cylinders to the auxiliary reservoir's pressure (3.19 bar), over MaxBP 2.8"}, + {"sn61", "brake cylinders between empty and the vehicle's highest pressure", + "its ESt3d_PZZ valve fills the cylinders to the auxiliary reservoir's pressure in an emergency (4.6 bar), over MaxBP 3.8"}, + {"sm03_v1/", "diesel engine turning forward, no faster than its top speed", "a lower gear at speed drives the engine over its top speed, by 10%"}, + {"wmb10_v1/", "diesel engine turning forward, no faster than its top speed", "a lower gear at speed drives the engine over its top speed, by 25%"}, + {"controls/full physics/", "no acceleration over 5 g", "the couplers' slack taken up as the train starts throws the wagons about at up to 5.7 g"}, + {"controls/full physics/", "distance counted as the speed runs", "the couplers' slack taken up as the train starts throws the wagons back within a step"}, +}; + +} // namespace + +bool excused(std::string_view const Trace, std::string_view const Invariant) { + return std::ranges::any_of(excuses, [&](excuse const &Excuse) { return Excuse.invariant == Invariant && Trace.find(Excuse.trace) != std::string_view::npos; }); +} + +} // namespace test diff --git a/tests/unit/invariants.h b/tests/unit/invariants.h new file mode 100644 index 000000000..491edc84b --- /dev/null +++ b/tests/unit/invariants.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +#include "McZapkie/MOVER.h" + +namespace test { + +// what the rules over time need of the step before +struct snapshot { + double time; + double V; + double DistCounter; +}; + +// a rule of physics or of the vehicle's circuits, which holds for every vehicle after every step, whatever +// the test does with it. every golden trace checks them as it samples (see golden.h), on every step, not only +// on the recorded ones, and with no comparison to a recorded file, so they hold on any compiler. a change which +// alters the behaviour on purpose records the traces anew, and the rules still check the new behaviour makes sense. +struct invariant { + std::string name; + // Before is null at the first step of a trace + std::function holds; + // the values the rule looks at, shown when it's broken + std::function explain; +}; + +std::vector const &invariants(); + +// the traces where the game breaks a rule, as it is, with the reason (see COVERAGE.md, what the tests found) +bool excused(std::string_view Trace, std::string_view Invariant); + +} // namespace test diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp index 451dd4c7b..cc801ba18 100644 --- a/tests/unit/main.cpp +++ b/tests/unit/main.cpp @@ -2,7 +2,7 @@ #include "stdafx.h" #include "doctest.h" -#include "utilities/Globals.h" +#include "vehicle.h" namespace { @@ -13,8 +13,8 @@ class deterministic_random : public doctest::IReporter { public: explicit deterministic_random(doctest::ContextOptions const &) {} - void test_case_start(doctest::TestCaseData const &) override { reseed(); } - void test_case_reenter(doctest::TestCaseData const &) override { reseed(); } + void test_case_start(doctest::TestCaseData const &) override { test::reseed_random(); } + void test_case_reenter(doctest::TestCaseData const &) override { test::reseed_random(); } // the rest of doctest's reporter interface: a listener reports nothing, the console reporter does that void report_query(doctest::QueryData const &) override { /* nothing to report */ } @@ -27,12 +27,6 @@ public: void log_assert(doctest::AssertData const &) override { /* nothing to report */ } void log_message(doctest::MessageData const &) override { /* nothing to report */ } void test_case_skipped(doctest::TestCaseData const &) override { /* nothing to report */ } - -private: - static void reseed() { - Global.random_engine.seed(1); - Global.local_random_engine.seed(1); - } }; } // namespace diff --git a/tests/unit/real_vehicles.h b/tests/unit/real_vehicles.h new file mode 100644 index 000000000..0928ca2d5 --- /dev/null +++ b/tests/unit/real_vehicles.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace test { + +// the real vehicle definitions of data/fiz, as paths for definition::fiz(): every .json file there, sorted. +// vehicles the game defines by including others are vehicles of their own too (201e-rn, included by 201e-w) +std::vector const &real_vehicles(); + +} // namespace test diff --git a/tests/unit/scenario.cpp b/tests/unit/scenario.cpp new file mode 100644 index 000000000..b933a0ab5 --- /dev/null +++ b/tests/unit/scenario.cpp @@ -0,0 +1,80 @@ +#include "stdafx.h" +#include "scenario.h" + +#include "McZapkie/hamulce.h" + +namespace test { + +scenario::scenario(std::string Name, std::vector Channels, track const &Track) : m_name(std::move(Name)), m_channels(std::move(Channels)), m_track(Track) { + reseed_random(); +} + +scenario &scenario::add(std::string const &Path, int Cab, int Coupling, double Velocity, double Gap) { + m_train.add(definition::fiz(Path), Cab, Coupling, Velocity, Gap); + return *this; +} + +void scenario::record() { + // on the first action, once the train is complete + if (false == m_traces.empty()) { + return; + } + for (std::size_t i = 0; i < m_train.size(); ++i) { + m_traces.push_back(std::make_unique(std::format("{}/{}", m_name, i), m_channels, 100)); + m_train[i].record(*m_traces.back()); + } +} + +void scenario::act(std::string const &Event) { + record(); + for (auto const &trace : m_traces) { + trace->mark(Event); + } +} + +void scenario::wait(double Seconds) { + record(); + drive(driver(), m_track, Seconds); +} + +bool scenario::start_up() { + record(); + return test::start_up(driver(), m_track, 300.0, m_traces.front().get()); +} + +void scenario::get_going() { + act("get going"); + release_brakes(cab()); + for (int i = 0; i < 3 && cab().DirActive <= 0; ++i) { + cab().DirectionForward(); + } + wait(20.0); +} + +void scenario::full_power(double Seconds) { + auto &controlling{driver().controlling}; + controlling.IncMainCtrl(1); + for (int i = 0; i < 90 && false == controlling.StLinFlag; ++i) { + wait(1.0); + } + for (int i = 0; i < 60 && false == controlling.IsMainCtrlMaxPowerPos(); ++i) { + controlling.IncMainCtrl(1); + wait(1.0); + } + wait(Seconds); +} + +void scenario::notch_back(int Position) { + auto &controlling{driver().controlling}; + while (controlling.MainCtrlPos > Position && controlling.DecMainCtrl(1)) { + wait(1.0); + } +} + +void scenario::check() const { + for (auto const &trace : m_traces) { + trace->check(); + } +} + +} // namespace test diff --git a/tests/unit/scenario.h b/tests/unit/scenario.h new file mode 100644 index 000000000..fff4d9619 --- /dev/null +++ b/tests/unit/scenario.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "consist.h" +#include "drive.h" +#include "golden.h" + +namespace test { + +// a scenario of a test: a train of real vehicles (one vehicle is a train too), a driver in the front one, +// and a golden trace for each vehicle (data/golden//.csv) with the events of +// the scenario in them: +// test::scenario scenario{"doors/elf", test::channels::motion() + test::channels::doors()}; +// scenario.add("elf_v1/21we-a", 1).add("elf_v1/21we-b").add("elf_v1/21we-c"); +// scenario.start_up(); +// scenario.act("open left"); +// scenario.cab().OperateDoors(side::left, true); +// scenario.wait(10.0); +// scenario.check(); +class scenario { +public: + scenario(std::string Name, std::vector Channels, track const &Track = {0.15, 0.0, catenary_3kv}); + + // adds a real vehicle at the back, see consist::add() + scenario &add(std::string const &Path, int Cab = 0, int Coupling = consist::full_coupling, double Velocity = 0.0, double Gap = 0.0); + + consist &train() { return m_train; } + TMoverParameters &vehicle(std::size_t Index) { return m_train[Index].mover(); } + TMoverParameters &cab() { return m_train[0].mover(); } + driven driver() { return driving(m_train); } + track &line() { return m_track; } + + // marks the event in the traces, the action follows it + void act(std::string const &Event); + // runs the train with the driver attending to it + void wait(double Seconds); + // gets the train ready to run, see test::start_up(); returns whether it did + bool start_up(); + // releases the brakes and sets the direction forward, for a train to move off + void get_going(); + // the main controller up to its full power position, a notch a second, and Seconds more there. the line + // contactors close at the first position, after their delay (InitialCtrlDelay) and once the pressure switch + // lets them (the cylinders under 2 bar, the pipe over 3.6), so it waits there for them, as the AI does + void full_power(double Seconds); + // the main controller back to the given position, a notch a second + void notch_back(int Position); + // checks the traces + void check() const; + +private: + void record(); + + std::string m_name; + std::vector m_channels; + track m_track; + consist m_train; + std::vector> m_traces; +}; + +} // namespace test diff --git a/tests/unit/test_auxiliaries.cpp b/tests/unit/test_auxiliaries.cpp new file mode 100644 index 000000000..bb823a4cf --- /dev/null +++ b/tests/unit/test_auxiliaries.cpp @@ -0,0 +1,112 @@ +#include "stdafx.h" +#include "doctest.h" +#include "scenario.h" +#include "drive.h" + +// characterisation of the auxiliary circuits of vehicles: the battery drained and charged, and the air of the +// pantographs kept in their own tank, whose pressure switch opens the line breaker, each in a scenario on real +// vehicles recorded in golden traces (data/golden/auxiliaries/) + +using test::scenario; +using namespace test::channels; + +namespace { + +std::vector auxiliaries() { + return { + {"BatteryVoltage", [](auto const &M) { return M.BatteryVoltage; }}, + {"Battery", [](auto const &M) { return M.Battery ? 1.0 : 0.0; }}, + {"ConverterFlag", [](auto const &M) { return M.ConverterFlag ? 1.0 : 0.0; }}, + {"Power110vIsAvailable", [](auto const &M) { return M.Power110vIsAvailable ? 1.0 : 0.0; }}, + {"PantPress", [](auto const &M) { return M.PantPress; }}, + {"PantVolume", [](auto const &M) { return M.PantVolume; }}, + {"PantCompFlag", [](auto const &M) { return M.PantCompFlag ? 1.0 : 0.0; }}, + {"PantPressSwitchActive", [](auto const &M) { return M.PantPressSwitchActive ? 1.0 : 0.0; }}, + {"PantPressLockActive", [](auto const &M) { return M.PantPressLockActive ? 1.0 : 0.0; }}, + }; +} + +} // namespace + +TEST_CASE("auxiliaries: the battery drained by the lights and the pantographs' compressor, until nothing starts") { + // without the converter the battery falls quickly to 90 V, then slowly as the lights and the small compressor + // draw on it; at 70 V (110 / 1.57) it opens the line breaker, and the locomotive doesn't start again + scenario scenario{"auxiliaries/battery/eu07 drained", traction() + auxiliaries()}; + scenario.add("4e_v1/4e", 1); + REQUIRE(scenario.start_up()); + auto &eu07{scenario.cab()}; + REQUIRE(eu07.BatteryVoltage == doctest::Approx(eu07.NominalBatteryVoltage)); + scenario.act("converter off, lights on, small compressor on"); + eu07.ConverterSwitch(false); + eu07.iLights[end::front] = 1 | 2 | 4; + eu07.iLights[end::rear] = 16; + eu07.PantCompFlag = true; + scenario.wait(30.0); + CHECK(eu07.BatteryVoltage < 0.83 * eu07.NominalBatteryVoltage); + CHECK(eu07.Mains); + scenario.wait(5000.0); + CHECK(eu07.BatteryVoltage < eu07.NominalBatteryVoltage / 1.57); + CHECK_FALSE(eu07.Mains); + scenario.act("start up again"); + CHECK_FALSE(scenario.start_up()); + CHECK(eu07.BatteryVoltage < eu07.NominalBatteryVoltage / 1.57); + scenario.check(); +} + +TEST_CASE("auxiliaries: the battery switched off, and charged by the converter") { + scenario scenario{"auxiliaries/battery/eu07 switched off", traction() + auxiliaries()}; + scenario.add("4e_v1/4e", 1); + REQUIRE(scenario.start_up()); + auto &eu07{scenario.cab()}; + scenario.act("converter off"); + eu07.ConverterSwitch(false); + scenario.wait(10.0); + auto const discharged{eu07.BatteryVoltage}; + CHECK(discharged < 0.83 * eu07.NominalBatteryVoltage); + // the converter charges it back at once + scenario.act("converter on"); + eu07.ConverterSwitch(true); + scenario.wait(10.0); + CHECK(eu07.BatteryVoltage == doctest::Approx(eu07.NominalBatteryVoltage)); + // switched off, the battery falls to 90 V at once, then holds + scenario.act("battery off"); + eu07.MainSwitch(false); + eu07.ConverterSwitch(false); + eu07.BatterySwitch(false); + scenario.wait(10.0); + CHECK(eu07.BatteryVoltage == doctest::Approx(discharged).epsilon(0.01)); + scenario.check(); +} + +TEST_CASE("auxiliaries: the pantographs on their own tank, leaking until the pressure switch opens the line breaker") { + // the pantographs cut off the main reservoir (bPantKurek3 off) are held up by the air of their own small tank, + // which leaks; under MinPress the pressure switch, primed at 4.6 bar, opens the line breaker. the small + // compressor pumps the tank up to 4.8 bar, priming the switch again. (an electric unit's switch turns its + // converter off instead; the EW58's pantographs have an automatic valve to the main reservoir, which keeps + // their tank up) + scenario scenario{"auxiliaries/pantograph tank/eu07", traction() + auxiliaries()}; + scenario.add("4e_v1/4e", 1); + REQUIRE(scenario.start_up()); + auto &eu07{scenario.cab()}; + // started up on the small compressor, the tank gets joined to the main reservoir, which primes the switch + scenario.act("tank joined to the main reservoir"); + eu07.bPantKurek3 = true; + scenario.wait(30.0); + REQUIRE(eu07.PantPressSwitchActive); + scenario.act("tank cut off the main reservoir"); + eu07.bPantKurek3 = false; + eu07.PantCompFlag = false; + scenario.wait(600.0); + CHECK(eu07.PantPress < eu07.EnginePowerSource.CollectorParameters.MinPress); + CHECK_FALSE(eu07.PantPressSwitchActive); + CHECK_FALSE(eu07.Mains); + scenario.act("small compressor on"); + eu07.PantCompFlag = true; + for (int i = 0; i < 60 && eu07.PantCompFlag; ++i) { + scenario.wait(10.0); + } + CHECK_FALSE(eu07.PantCompFlag); + CHECK(eu07.PantPress >= 4.6); // stopped at 4.8, leaking since + CHECK(eu07.PantPressSwitchActive); + scenario.check(); +} diff --git a/tests/unit/test_brake_cycle.cpp b/tests/unit/test_brake_cycle.cpp new file mode 100644 index 000000000..1dc5a84c1 --- /dev/null +++ b/tests/unit/test_brake_cycle.cpp @@ -0,0 +1,194 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "golden.h" +#include "real_vehicles.h" + +#include "McZapkie/hamulce.h" + +#include +#include + +using test::definition; +using test::vehicle; + +// characterisation of the brakes of every real vehicle: the brake pipe driven through a whole cycle, as the +// locomotive of a train would drive it, and the distributor valve, the cylinders and the reservoirs followed +// in golden traces (data/golden/brake cycle/). pressures in bar, the pipe runs at 5.0 + +namespace { + +struct stage { + char const *name; + double pipe; // held brake pipe pressure + double seconds; +}; + +// charging, the service application in steps, release, the overcharge used to speed the release up and its +// drop back, the emergency application and recharging after it +std::array const cycle{{ + {"charge", 5.0, 90.0}, {"light", 4.6, 20.0}, {"medium", 4.2, 20.0}, {"full service", 3.5, 20.0}, + {"release", 5.0, 40.0}, {"overcharge", 5.4, 15.0}, {"back to 5", 5.0, 45.0}, {"emergency", 0.0, 15.0}, + {"recharge", 5.0, 90.0}, +}}; + +// returns the pressure in the cylinders at the end of each stage, by the name of the stage +std::map> run_cycle(vehicle &Vehicle, test::trace &Trace) { + std::map> cylinders; + for (auto const &stage : cycle) { + Trace.mark(stage.name); + Vehicle.hold_brake_pipe(stage.pipe); + Vehicle.run(stage.seconds); + cylinders[stage.name] = Vehicle->BrakePress; + } + return cylinders; +} + +} // namespace + +TEST_CASE("brake cycle: every real vehicle through charge, service steps, release, overcharge and emergency") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle real{definition::fiz(path)}; + test::trace trace{"brake cycle/" + path, test::channels::brakes(), 100}; + real.record(trace); + // a vehicle braked by the pipe: released once charged, braking harder by every step of the service + // application, and by the emergency one. the release isn't checked: some vehicles don't release within + // its 40 s, others keep a little air after the overcharge + if (auto cylinders{run_cycle(real, trace)}; cylinders["full service"] > 0.0) { + CHECK(cylinders["charge"] < 0.1); + CHECK(cylinders["light"] <= cylinders["medium"]); + CHECK(cylinders["medium"] <= cylinders["full service"] + 0.01); + CHECK(cylinders["full service"] > 1.0); + CHECK(cylinders["emergency"] > 0.5); + } + else { + // the draisine, the road vehicle and the catenary car, which have no brake worked by the pipe + CHECK(cylinders["emergency"] == 0.0); + } + trace.check(); + } +} + +TEST_CASE("brake cycle: every brake delay setting of the real vehicles") { + // G, P, R and their mixes change how fast the cylinders fill and empty; every vehicle offering more + // than one setting goes through the service part of the cycle on each + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle probe{definition::fiz(path)}; + for (auto const delay : {bdelay_G, bdelay_P, bdelay_R, bdelay_R + bdelay_M, bdelay_G + bdelay_M}) { + if ((probe->BrakeDelays & delay) != delay || delay == probe->BrakeDelayFlag) { + continue; + } + CAPTURE(delay); + test::reseed_random(); + vehicle real{definition::fiz(path)}; + REQUIRE(real->BrakeDelaySwitch(delay)); + test::trace trace{std::format("brake delays/{} {}", path, delay), test::channels::brakes(), 100}; + real.record(trace); + std::map> cylinders; + for (auto const &stage : {stage{"charge", 5.0, 90.0}, stage{"full service", 3.5, 30.0}, stage{"release", 5.0, 60.0}}) { + trace.mark(stage.name); + real.hold_brake_pipe(stage.pipe); + real.run(stage.seconds); + cylinders[stage.name] = real->BrakePress; + } + // every setting brakes in half a minute and releases in a minute + CHECK(cylinders["full service"] > 1.0); + CHECK(cylinders["release"] < 0.3); + trace.check(); + } + } +} + +TEST_CASE("brake cycle: the valves the game knows but none of its vehicles has, fitted to a wagon") { + // the variants of the Westinghouse and Knorr valves, the Hik ones, the old ESt3 and the CV1 ones, with a name + // the game doesn't know at the end. any other name with ESt in it is an Oerlikon valve (NESt3) whose name sets + // its relays: -ED the electrodynamic brake's one, AL2 the load-weighing one, without ESt3 the rapid one + for (auto const *name : {"W_Lu_L", "W_Lu_XR", "W_Lu_VI", "Kg", "Kp", "Kss", "Kkg", "Kkp", "Kks", "Hikp1", "Hikss", "Hikg1", "SW", "NESt3", "M483", "CV1", "CV1_L_TR", + "CV1_R", "ESt4-ED", "ESt4AL2", "no such valve"}) { + CAPTURE(name); + test::reseed_random(); + vehicle real{definition::fiz("3wl_v2/3wl").set("Brake", "BrakeValve", name)}; + test::trace trace{std::string{"brake cycle/fitted/"} + name, test::channels::brakes(), 100}; + real.record(trace); + run_cycle(real, trace); + trace.check(); + } +} + +TEST_CASE("brake cycle: the rapid setting at speed, the anti-slip brake and the releaser button") { + // R gives the cylinders more air at speed (RM); the anti-slip request the game makes while the wheels slip and + // slow down (3) and the releaser button (ub_Release) let the LSt's cylinders out, the KE's not + struct valve { + char const *name; + char const *path; + int delay; + double service; // in the cylinders, full service over 100 km/h + bool releases; // by the button + }; + for (auto const &[name, path, delay, service, releases] : {valve{"KE", "z1_v2/152a", bdelay_R, 3.78, false}, valve{"KE in P", "z1_v2/152a", bdelay_P, 2.70, false}, + valve{"LSt", "4e_v1/4e", bdelay_R, 5.79, true}, valve{"LSt in P", "4e_v1/4e", bdelay_P, 3.68, true}}) { + CAPTURE(name); + test::reseed_random(); + vehicle real{definition::fiz(path), 160.0}; + real->DecManualBrakeLevel(ManualBrakePosNo); + real->BrakeDelaySwitch(delay); // false when already set + REQUIRE(real->BrakeDelayFlag == delay); + using test::channels::operator+; + test::trace trace{std::string{"brake cycle/at speed/"} + name, test::channels::motion() + test::channels::brakes(), 100}; + real.record(trace); + trace.mark("charge"); + real.hold_brake_pipe(5.0); + real.run(60.0); + trace.mark("full service"); + real.hold_brake_pipe(3.5); + real.run(10.0); + CHECK(real->Vel > 100.0); + CHECK(real->BrakePress == doctest::Approx(service).epsilon(0.01)); + trace.mark("anti-slip"); + real->Hamulec->ASB(3); + real.run(2.0); + real->Hamulec->ASB(0); + real.run(3.0); + trace.mark("releaser button"); + real->Hamulec->SetUniversalFlag(TUniversalBrake::ub_Release); + real.run(3.0); + real->Hamulec->SetUniversalFlag(0); + CHECK((real->BrakePress < 1.0) == releases); + // released for good, the pipe still at full service + real.run(40.0); + CHECK((real->BrakePress < 0.1) == releases); + trace.check(); + } +} + +TEST_CASE("brake cycle: every material of the brake shoes and disks, braking a wagon from speed") { + // the friction of the shoes (BM) against the speed and the pressing force (friction.cpp): the game's wagons + // have P10-Bg, P10-Bgu, Cosid and the disks; FR513, FR510, P10yBg and P10yBgu none of them, nor an unknown one + // (P10, as with no BM). Mg adds the magnetic rail brake under 3 bar in the pipe + for (auto const *material : {"P10-Bg", "P10-Bgu", "FR513", "FR510", "Cosid", "P10yBg", "P10yBgu", "Disk1", "Disk1+Mg", "Disk2", "no such material"}) { + CAPTURE(material); + test::reseed_random(); + vehicle real{definition::fiz("3wl_v2/3wl").set("Brake", "BM", material), 100.0}; + real->DecManualBrakeLevel(ManualBrakePosNo); + using test::channels::operator+; + test::trace trace{std::string{"brake cycle/materials/"} + material, test::channels::motion() + test::channels::brakes(), 100}; + real.record(trace); + trace.mark("charge"); + real.hold_brake_pipe(5.0); + real.run(30.0); + auto const speed{real->Vel}; + trace.mark("full service"); + real.hold_brake_pipe(3.5); + real.run(20.0); + CHECK(real->Vel < speed - 10.0); + CHECK(real->Fb > 0.0); + trace.mark("emergency"); + real.hold_brake_pipe(0.0); + real.run(60.0); + trace.check(); + } +} diff --git a/tests/unit/test_brake_handles.cpp b/tests/unit/test_brake_handles.cpp new file mode 100644 index 000000000..51c25243b --- /dev/null +++ b/tests/unit/test_brake_handles.cpp @@ -0,0 +1,154 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "golden.h" +#include "real_vehicles.h" +#include "drive.h" + +#include "McZapkie/hamulce.h" + +using test::definition; +using test::vehicle; + +// characterisation of the driver's brake handles and the local brake handles of the real locomotives and +// units: every position of the handle, with the main reservoir kept full, followed in golden traces +// (data/golden/brake handles/). the vehicle runs alone, so the handle drives only its own brake pipe + +namespace { + +std::vector handle_channels() { + using namespace test::channels; + return brakes() + handles() + air(); +} + +// the vehicles with a driver's brake handle +bool has_handle(vehicle &Vehicle) { + return Vehicle->BrakeCtrlPosNo > 1; +} + +void hold(vehicle &Vehicle, test::trace &Trace, std::string const &Event, double Position, double Seconds) { + Trace.mark(Event); + Vehicle->BrakeLevelSet(Position); + Vehicle.run(Seconds); +} + +// every position of the driver's handle, recorded in the trace +void every_position(vehicle &Vehicle, test::trace &Trace) { + Vehicle.hold_main_reservoir(std::max(Vehicle->MaxCompressor, 8.0)); + auto &handle{*Vehicle->Handle}; + Vehicle.record(Trace); + + hold(Vehicle, Trace, "running", handle.GetPos(bh_RP), 90.0); + // every position from the lowest to the highest, emergency included, then back to running + auto const lowest{static_cast(std::ceil(handle.GetPos(bh_MIN)))}; + auto const highest{static_cast(std::floor(handle.GetPos(bh_MAX)))}; + for (auto position = lowest; position <= highest; ++position) { + hold(Vehicle, Trace, std::format("position {}", position), position, 15.0); + hold(Vehicle, Trace, "running", handle.GetPos(bh_RP), 30.0); + } + // filling stroke, neutral and the positions of the ep brake, where the handle has them + hold(Vehicle, Trace, "filling stroke", handle.GetPos(bh_FS), 10.0); + hold(Vehicle, Trace, "neutral", handle.GetPos(bh_NP), 20.0); + hold(Vehicle, Trace, "first step", handle.GetPos(bh_MB), 20.0); + hold(Vehicle, Trace, "full service", handle.GetPos(bh_FB), 20.0); + hold(Vehicle, Trace, "ep release", handle.GetPos(bh_EPR), 10.0); + hold(Vehicle, Trace, "ep hold", handle.GetPos(bh_EPN), 10.0); + hold(Vehicle, Trace, "ep brake", handle.GetPos(bh_EPB), 10.0); + hold(Vehicle, Trace, "running", handle.GetPos(bh_RP), 60.0); +} + +} // namespace + +TEST_CASE("brake handles: every position of the driver's handle of every real vehicle") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle real{definition::fiz(path)}; + if (false == has_handle(real)) { + continue; + } + test::trace trace{"brake handles/" + path, handle_channels(), 100}; + every_position(real, trace); + trace.check(); + } +} + +TEST_CASE("brake handles: the handles the game knows but none of its vehicles has, fitted to an EU07") { + // test is a handle for trying things out, D2 and Westinghouse fall back to the plain handle of the base class + for (auto const *name : {"test", "D2", "Westinghouse", "MHZ_EN57", "St113", "no such handle"}) { + CAPTURE(name); + test::reseed_random(); + vehicle real{definition::fiz("4e_v1/4e").set("Cntrl", "BrakeHandle", name)}; + test::trace trace{std::string{"brake handles/fitted/"} + name, handle_channels(), 100}; + every_position(real, trace); + trace.check(); + } +} + +TEST_CASE("brake handles: the local brake handle of every real vehicle") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle real{definition::fiz(path)}; + if (false == has_handle(real)) { + continue; + } + real.hold_main_reservoir(std::max(real->MaxCompressor, 8.0)); + test::trace trace{"local brake/" + path, handle_channels(), 100}; + real.record(trace); + real->BrakeLevelSet(real->Handle->GetPos(bh_RP)); + trace.mark("charge"); + real.run(60.0); + for (auto const position : {0.25, 0.5, 1.0, 0.5, 0.0}) { + trace.mark(std::format("local {:f}", position)); + real->LocalBrakePosA = position; + real.run(15.0); + } + trace.check(); + } +} + +TEST_CASE("brake handles: the overload and the high pressure buttons of the MHZ handles") { + // the overload button charges the pipe over its running pressure (TP), which then decays; the high pressure + // button gives the filling stroke its overpressure, where the handle has one only with the button pressed. + // from the cab, with the battery on and the driver attending to the vigilance: the electro-pneumatic handles + // work only so + // the MHZ_EN57, whose unit doesn't charge its pipe alone, fitted to an EU07 + for (auto const &[name, definition] : std::vector>{ + {"eu47", definition::fiz("e186_v2/eu47")}, + {"e6act", definition::fiz("e6act_v2/e6acta")}, + {"eu07 with MHZ_EN57", definition::fiz("4e_v1/4e").set("Cntrl", "BrakeHandle", "MHZ_EN57")}}) { + CAPTURE(name); + test::reseed_random(); + vehicle real{definition, 0.0, "test", 1}; + real->BatterySwitch(true); + real->CabActivisation(); + real.hold_main_reservoir(std::max(real->MaxCompressor, 8.0)); + test::trace trace{"brake handles/buttons/" + name, handle_channels(), 100}; + real.record(trace); + auto &handle{*real->Handle}; + auto const at{[&trace, &real](std::string const &Event, double Position, double Seconds) { + trace.mark(Event); + real->BrakeLevelSet(Position); + test::drive(test::driving(real), {}, Seconds); + }}; + at("running", handle.GetPos(bh_RP), 60.0); + auto const running{real->PipePress}; + handle.SetUniversalFlag(TUniversalBrake::ub_Overload); + at("overload button", handle.GetPos(bh_RP), 30.0); + handle.SetUniversalFlag(0); + auto const overloaded{real->PipePress}; + at("overload decaying", handle.GetPos(bh_RP), 240.0); + auto const decayed{real->PipePress}; + handle.SetUniversalFlag(TUniversalBrake::ub_HighPressure); + at("high pressure button, filling stroke", handle.GetPos(bh_FS), 10.0); + auto const filled{real->PipePress}; + handle.SetUniversalFlag(0); + at("running", handle.GetPos(bh_RP), 120.0); + CHECK(running == doctest::Approx(5.0).epsilon(0.01)); + CHECK(overloaded > running + 0.3); + CHECK(decayed < overloaded); + CHECK(filled >= decayed - 0.05); + trace.check(); + } +} diff --git a/tests/unit/test_cab.cpp b/tests/unit/test_cab.cpp new file mode 100644 index 000000000..4c5ba9f53 --- /dev/null +++ b/tests/unit/test_cab.cpp @@ -0,0 +1,229 @@ +#include "stdafx.h" +#include "doctest.h" +#include "consist.h" +#include "scenario.h" +#include "drive.h" + +#include "McZapkie/hamulce.h" +#include "utilities/utilities.h" + +// characterisation of what the cab (Train.cpp) and the AI (Driver.cpp, driverhints.cpp) call on a vehicle +// besides the controls of test_controls.cpp: the gauges and lamps reading other vehicles of the train, the +// controller of the electrodynamic brake apart from the local brake, the spring brake, the ep brake's fuse, +// cutting off damaged motors, the cab activated by itself, a road vehicle's place across the road, and the +// lamps and the buzzer of the security system + +using test::consist; +using test::definition; +using test::scenario; +using test::vehicle; +using namespace test::channels; + +TEST_CASE("cab: the engine rotation of up to three vehicles in multiple, on the tachometers of the SN61") { + consist train; + auto &first{train.add(definition::fiz("sn61_v2/sn61"), 1).mover()}; + auto &second{train.add(definition::fiz("sn61_v2/sn61")).mover()}; + auto &third{train.add(definition::fiz("sn61_v2/sn61")).mover()}; + first.enrot = -10.0; + second.enrot = 20.0; + third.enrot = 30.0; + CHECK(first.ShowEngineRotation(1) == doctest::Approx(10.0)); + CHECK(first.ShowEngineRotation(2) == doctest::Approx(20.0)); + CHECK(first.ShowEngineRotation(3) == doctest::Approx(30.0)); + CHECK(first.ShowEngineRotation(4) == doctest::Approx(0.0)); + // the others look the same way along their couplers, front first + CHECK(second.ShowEngineRotation(2) == doctest::Approx(10.0)); + CHECK(third.ShowEngineRotation(2) == doctest::Approx(20.0)); + CHECK(third.ShowEngineRotation(3) == doctest::Approx(10.0)); + // no control cable, no reading + consist apart; + auto const &alone{apart.add(definition::fiz("sn61_v2/sn61"), 1).mover()}; + apart.add(definition::fiz("sn61_v2/sn61"), 0, coupling::coupler | coupling::brakehose); + CHECK(alone.ShowEngineRotation(2) == doctest::Approx(0.0)); +} + +TEST_CASE("cab: the lamps of the overload relay and the starting resistors, read through the control cable") { + consist unit; + auto &trailer{unit.add(definition::fiz("ed72_v1/5bs-ra"), 1).mover()}; + auto &motor{unit.add(definition::fiz("ed72_v1/6bs-sa")).mover()}; + REQUIRE(trailer.Power < 0.01); + REQUIRE(motor.Power > 0.01); + CHECK_FALSE(trailer.FuseFlagCheck()); + CHECK_FALSE(trailer.ResistorsFlagCheck()); + motor.FuseFlag = true; + motor.ResistorsFlag = true; + CHECK(trailer.FuseFlagCheck()); + CHECK(trailer.ResistorsFlagCheck()); + CHECK(motor.FuseFlagCheck()); + CHECK(motor.ResistorsFlagCheck()); +} + +TEST_CASE("cab: the engine's idle speed, and the ET42's second controller shown in its dynamic brake") { + vehicle sn61{definition::fiz("sn61_v2/sn61")}; + CHECK(sn61->EngineIdleRPM() == doctest::Approx(sn61->dizel_nmin * 60.0)); + vehicle su45{definition::fiz("su45_v2/301d")}; + CHECK(su45->EngineIdleRPM() == doctest::Approx(su45->DElist[su45->MainCtrlNoPowerPos()].RPM)); + vehicle eu07{definition::fiz("4e_v1/4e")}; + CHECK(eu07->EngineIdleRPM() == std::numeric_limits::max()); + + vehicle et42{definition::fiz("et42_v2/112e-a")}; + CHECK(et42->GetVirtualScndPos() == 0); + et42->DynamicBrakeFlag = true; + CHECK(et42->GetVirtualScndPos() == -1); + et42->ScndCtrlPos = 1; + CHECK(et42->GetVirtualScndPos() == 1); + // with a speed control in place of the second controller, either end of it, for the AI + vehicle railbus{definition::fiz("21xm_v1/214ma")}; + REQUIRE(railbus->SpeedCtrl); + railbus->ScndCtrlPos = 1; + CHECK(railbus->IsScndCtrlNoPowerPos()); + CHECK(railbus->IsScndCtrlMaxPowerPos()); + CHECK(eu07->IsScndCtrlNoPowerPos()); + CHECK_FALSE(eu07->IsScndCtrlMaxPowerPos()); +} + +TEST_CASE("cab: the controller of the electrodynamic brake apart from the local brake") { + // none of the game's vehicles has one yet (SplitEDPneumaticBrake): the electrodynamic brake follows its own + // controller of DBPN positions, the local brake is pneumatic alone + scenario scenario{"cab/split electrodynamic brake", motion() + traction() + brakes()}; + scenario.add("e186_v2/eu47", 1, consist::full_coupling, 100.0).add("bdhpumn_v3/bdhpumn", 0, consist::full_coupling, 100.0); + auto &eu47{scenario.cab()}; + CHECK_FALSE(eu47.IncDynamicBrakeLevel(1.f)); + CHECK(eu47.DynamicBrakeRatio() == doctest::Approx(0.0)); + + scenario.train()[0].mover().SplitEDPneumaticBrake = true; + REQUIRE(scenario.start_up()); + REQUIRE(eu47.DynamicBrakeCtrlPosNo == 10); + CHECK(eu47.DynamicBrakeAvailable()); + scenario.act("dynamic brake up"); + for (int i = 0; i < 5; ++i) { + CHECK(eu47.IncDynamicBrakeLevel(1.f)); + } + CHECK(eu47.DynamicBrakeRatio() == doctest::Approx(0.5)); + scenario.wait(10.0); + CHECK(eu47.DynamicBrakeFlag); + CHECK(eu47.Im < -50.0); + CHECK(eu47.BrakePress < 0.1); + scenario.act("dynamic brake full"); + CHECK(eu47.DynamicBrakeLevelSet(2.0)); + CHECK(eu47.DynamicBrakeRatio() == doctest::Approx(1.0)); + CHECK_FALSE(eu47.IncDynamicBrakeLevel(1.f)); + scenario.wait(10.0); + scenario.act("dynamic brake off"); + for (int i = 0; i < 20 && eu47.DecDynamicBrakeLevel(1.f); ++i) { + // down a notch at a time + } + CHECK(eu47.DynamicBrakeRatio() == doctest::Approx(0.0)); + // the local brake no longer brings the motors in + scenario.act("local brake"); + eu47.IncLocalBrakeLevel(LocalBrakePosNo); + scenario.wait(5.0); + CHECK(eu47.BrakePress > 1.0); + scenario.check(); +} + +TEST_CASE("cab: the spring brake shut off and released by hand, the ep brake's fuse, the train brake handle stepped") { + scenario scenario{"cab/214m spring brake", motion() + brakes()}; + scenario.add("21xm_v1/214ma", 1); + REQUIRE(scenario.start_up()); + auto &railbus{scenario.cab()}; + scenario.act("spring brake on"); + railbus.SpringBrakeActivate(true); + scenario.wait(10.0); + CHECK(railbus.SpringBrake.IsActive); + // released by hand only with its cylinder empty of the air holding it off + auto const pressure{railbus.SpringBrake.Cylinder->P()}; + CHECK(railbus.SpringBrakeRelease() == (pressure < railbus.SpringBrake.MinForcePressure)); + scenario.act("shut off"); + CHECK(railbus.SpringBrakeShutOff(true)); + CHECK_FALSE(railbus.SpringBrakeShutOff(true)); + scenario.wait(5.0); + CHECK(railbus.SpringBrakeShutOff(false)); + scenario.act("spring brake off"); + railbus.SpringBrakeActivate(false); + scenario.wait(10.0); + // the ep brake's fuse + CHECK(railbus.EpFuseSwitch(false)); + CHECK_FALSE(railbus.EpFuseSwitch(false)); + CHECK(railbus.EpFuseSwitch(true)); + // the train brake handle stepped as the AI does, false at its ends + scenario.act("handle stepped"); + railbus.BrakeLevelSet(railbus.Handle->GetPos(bh_RP)); + CHECK(railbus.BrakeLevelAdd(1.0)); + CHECK(railbus.BrakeCtrlPos == static_cast(railbus.Handle->GetPos(bh_RP)) + 1); + CHECK(railbus.BrakeLevelAdd(-1.0)); + CHECK_FALSE(railbus.BrakeLevelAdd(-10.0)); + CHECK_FALSE(railbus.BrakeLevelAdd(100.0)); + scenario.wait(5.0); + scenario.check(); +} + +TEST_CASE("cab: damaged motors cut off in a car without a driver") { + // cutting off takes the damage off the engine and half of the powered axles with it; only in a car nobody + // drives, with its engine damaged + vehicle driven{definition::fiz("ew58_v1/ew58-sa"), 0.0, "test", 1}; + driven->BatterySwitch(true); + REQUIRE(driven->CabActivisation()); + driven->DamageFlag |= dtrain_engine; + CHECK_FALSE(driven->CutOffEngine()); + vehicle car{definition::fiz("ew58_v1/ew58-sa")}; + auto const axles{car->NPoweredAxles}; + CHECK_FALSE(car->CutOffEngine()); + car->DamageFlag |= dtrain_engine; + CHECK(car->CutOffEngine()); + CHECK(car->NPoweredAxles == axles / 2); + CHECK_FALSE(TestFlag(car->DamageFlag, dtrain_engine)); + vehicle wagon{definition::fiz("3wl_v2/3wl")}; + wagon->DamageFlag |= dtrain_engine; + CHECK_FALSE(wagon->CutOffEngine()); +} + +TEST_CASE("cab: activated by itself, where the vehicle allows it") { + for (auto const allowed : {false, true}) { + CAPTURE(allowed); + vehicle eu07{definition::fiz("4e_v1/4e").set("Cntrl", "AutomaticCabActivation", allowed ? "Yes" : "No"), 0.0, "test", 1}; + eu07->BatterySwitch(true); + CHECK(eu07->CabActivisationAuto() == allowed); + CHECK((eu07->CabActive != 0) == allowed); + CHECK(eu07->CabDeactivisationAuto() == allowed); + CHECK(eu07->CabActive == 0); + } +} + +TEST_CASE("cab: a road vehicle moved across its road") { + vehicle bus{definition::fiz("road/man/nl293")}; + bus.step(test::track{0.15, 0.0, 0.0, 2, 1, 7.0}); + REQUIRE(TestFlag(bus->CategoryFlag, 2)); + CHECK(bus->ChangeOffsetH(0.5)); + CHECK(bus->OffsetTrackH == doctest::Approx(0.5)); + // off the edge of the road + CHECK_FALSE(bus->ChangeOffsetH(100.0)); + vehicle eu07{definition::fiz("4e_v1/4e")}; + CHECK_FALSE(eu07->ChangeOffsetH(0.5)); +} + +TEST_CASE("cab: the lamps and the buzzer of the security system left unattended") { + scenario scenario{"cab/security system", motion() + brakes()}; + scenario.add("4e_v1/4e", 1); + REQUIRE(scenario.start_up()); + auto &eu07{scenario.cab()}; + scenario.get_going(); + scenario.full_power(10.0); + auto const &security{eu07.SecuritySystem}; + CHECK_FALSE(security.is_beeping()); + scenario.act("unattended"); + // the train runs with nobody acknowledging the vigilance + bool blinked{false}; + bool beeped{false}; + for (int i = 0; i < 120; ++i) { + scenario.train().run(1.0, scenario.line()); + blinked |= security.is_vigilance_blinking(); + beeped |= security.is_beeping(); + } + CHECK(blinked); + CHECK(beeped); + CHECK_FALSE(security.is_cabsignal_beeping()); + CHECK(eu07.PipePress < 1.0); + CHECK(security.radiostop_available()); + scenario.check(); +} diff --git a/tests/unit/test_commands.cpp b/tests/unit/test_commands.cpp new file mode 100644 index 000000000..6f5730cf7 --- /dev/null +++ b/tests/unit/test_commands.cpp @@ -0,0 +1,547 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "golden.h" +#include "consist.h" +#include "real_vehicles.h" + +#include +#include + +#include "McZapkie/hamulce.h" + +// characterisation of the commands vehicles pass each other along the couplers (RunCommand): what each of them +// does to every real vehicle, recorded in golden traces (data/golden/commands/), how they travel through a train of +// vehicles coupled either way round, and the loading and unloading driven by them + +using test::definition; +using test::vehicle; + +namespace { + +double flag(bool Value) { + return Value ? 1.0 : 0.0; +} + +// what the commands change +std::vector state() { + std::vector result{ + {"MainCtrlPos", [](auto const &M) { return M.MainCtrlPos; }}, + {"ScndCtrlPos", [](auto const &M) { return M.ScndCtrlPos; }}, + {"LastRelayTime", [](auto const &M) { return M.LastRelayTime; }}, + {"eimic_real", [](auto const &M) { return M.eimic_real; }}, + {"TUHEX_StageActual", [](auto const &M) { return M.TUHEX_StageActual; }}, + {"TUHEX_Active", [](auto const &M) { return flag(M.TUHEX_Active); }}, + {"DynamicBrakeEMUStatus", [](auto const &M) { return flag(M.DynamicBrakeEMUStatus); }}, + {"BrakeReleaser", [](auto const &M) { return flag(M.Hamulec->Releaser()); }}, + {"WaterPumpBreaker", [](auto const &M) { return flag(M.WaterPump.breaker); }}, + {"WaterPumpEnabled", [](auto const &M) { return flag(M.WaterPump.is_enabled); }}, + {"WaterPumpDisabled", [](auto const &M) { return flag(M.WaterPump.is_disabled); }}, + {"WaterHeaterBreaker", [](auto const &M) { return flag(M.WaterHeater.breaker); }}, + {"WaterHeaterEnabled", [](auto const &M) { return flag(M.WaterHeater.is_enabled); }}, + {"WaterCircuitsLink", [](auto const &M) { return flag(M.WaterCircuitsLink); }}, + {"FuelPumpEnabled", [](auto const &M) { return flag(M.FuelPump.is_enabled); }}, + {"FuelPumpDisabled", [](auto const &M) { return flag(M.FuelPump.is_disabled); }}, + {"OilPumpEnabled", [](auto const &M) { return flag(M.OilPump.is_enabled); }}, + {"OilPumpDisabled", [](auto const &M) { return flag(M.OilPump.is_disabled); }}, + {"CompartmentLightsEnabled", [](auto const &M) { return flag(M.CompartmentLights.is_enabled); }}, + {"CompartmentLightsDisabled", [](auto const &M) { return flag(M.CompartmentLights.is_disabled); }}, + {"MainSwitch", [](auto const &M) { return flag(M.Mains); }}, + {"Heating", [](auto const &M) { return flag(M.Heating); }}, + {"DirActive", [](auto const &M) { return M.DirActive; }}, + {"DirAbsolute", [](auto const &M) { return M.DirAbsolute; }}, + {"CabActive", [](auto const &M) { return M.CabActive; }}, + {"CabMaster", [](auto const &M) { return flag(M.CabMaster); }}, + {"AutoRelayFlag", [](auto const &M) { return flag(M.AutoRelayFlag); }}, + {"ConverterAllow", [](auto const &M) { return flag(M.ConverterAllow); }}, + {"Battery", [](auto const &M) { return flag(M.Battery); }}, + {"CompressorAllow", [](auto const &M) { return flag(M.CompressorAllow); }}, + {"CompressorListPos", [](auto const &M) { return M.CompressorListPos; }}, + {"DoorLock", [](auto const &M) { return flag(M.Doors.lock_enabled); }}, + {"DoorStep", [](auto const &M) { return flag(M.Doors.step_enabled); }}, + {"DoorRemoteOnly", [](auto const &M) { return flag(M.Doors.remote_only); }}, + {"DepartureSignal", [](auto const &M) { return flag(M.DepartureSignal); }}, + {"PantsValve", [](auto const &M) { return flag(M.PantsValve.is_enabled) + 2 * flag(M.PantsValve.is_disabled); }}, + {"MaxCurrentHigh", [](auto const &M) { return flag(M.MotorOverloadRelayHighThreshold); }}, + {"Imin", [](auto const &M) { return M.Imin; }}, + {"DamageFlag", [](auto const &M) { return M.DamageFlag; }}, + {"RadioStop", [](auto const &M) { return flag(M.RadioStopFlag); }}, + {"BrakeDelayFlag", [](auto const &M) { return M.BrakeDelayFlag; }}, + {"SandDose", [](auto const &M) { return flag(M.SandDose); }}, + {"CabSignal", [](auto const &M) { return flag(M.SecuritySystem.is_cabsignal_blinking()); }}, + {"LoadAmount", [](auto const &M) { return M.LoadAmount; }}, + {"LoadStatus", [](auto const &M) { return M.LoadStatus; }}, + {"SpeedCtrlValue", [](auto const &M) { return M.SpeedCtrlValue; }}, + {"SpeedCtrlParking", [](auto const &M) { return flag(M.SpeedCtrlUnit.Parking); }}, + {"SpringBrakeActivate", [](auto const &M) { return flag(M.SpringBrake.Activate); }}, + }; + for (int end : {end::front, end::rear}) { + auto const name{std::string{end == end::front ? "Front" : "Rear"}}; + result.emplace_back("MotorBlowers" + name + "Enabled", [end](auto const &M) { return flag(M.MotorBlowers[end].is_enabled); }); + result.emplace_back("MotorBlowers" + name + "Disabled", [end](auto const &M) { return flag(M.MotorBlowers[end].is_disabled); }); + result.emplace_back("Pant" + name + "Valve", [end](auto const &M) { return flag(M.Pantographs[end].valve.is_enabled) + 2 * flag(M.Pantographs[end].valve.is_disabled); }); + } + for (int side : {side::right, side::left}) { + auto const name{std::string{side == side::right ? "Right" : "Left"}}; + result.emplace_back(name + "DoorPermit", [side](auto const &M) { return flag(M.Doors.instances[side].open_permit); }); + result.emplace_back(name + "DoorRemoteOpen", [side](auto const &M) { return flag(M.Doors.instances[side].remote_open); }); + result.emplace_back(name + "DoorRemoteClose", [side](auto const &M) { return flag(M.Doors.instances[side].remote_close); }); + } + return result; +} + +struct command { + std::string name; + double value1; + double value2{1.0}; // the direction it comes from, 1 the front +}; + +// every command RunCommand knows, with values taking each of its ways, and one it doesn't +std::vector const commands{ + {"MainCtrl", 1}, + {"MainCtrl", 2}, + {"MainCtrl", 0}, + {"MainCtrl", 99}, + {"ScndCtrl", 1}, + {"ScndCtrl", 0}, + {"ScndCtrl", 99}, + {"EIMIC", 0.5}, + {"EIMIC", -0.5}, + {"Brake", 1}, + {"Brake", 2}, + {"Brake", 0}, + {"BrakeReleaser", 1}, + {"BrakeReleaser", 0}, + {"WaterPumpBreakerSwitch", 1}, + {"WaterPumpSwitch", 1}, + {"WaterPumpSwitchOff", 1}, + {"WaterPumpSwitchOff", 0}, + {"WaterPumpSwitch", 0}, + {"WaterPumpBreakerSwitch", 0}, + {"WaterHeaterBreakerSwitch", 1}, + {"WaterHeaterSwitch", 1}, + {"WaterHeaterSwitch", 0}, + {"WaterHeaterBreakerSwitch", 0}, + {"WaterCircuitsLinkSwitch", 1}, + {"WaterCircuitsLinkSwitch", 0}, + {"FuelPumpSwitch", 1}, + {"FuelPumpSwitchOff", 1}, + {"FuelPumpSwitchOff", 0}, + {"FuelPumpSwitch", 0}, + {"OilPumpSwitch", 1}, + {"OilPumpSwitchOff", 1}, + {"OilPumpSwitchOff", 0}, + {"OilPumpSwitch", 0}, + {"MotorBlowersFrontSwitch", 1}, + {"MotorBlowersFrontSwitchOff", 1}, + {"MotorBlowersFrontSwitchOff", 0}, + {"MotorBlowersFrontSwitch", 0}, + {"MotorBlowersRearSwitch", 1}, + {"MotorBlowersRearSwitchOff", 1}, + {"MotorBlowersRearSwitchOff", 0}, + {"MotorBlowersRearSwitch", 0}, + {"CompartmentLightsSwitch", 1}, + {"CompartmentLightsSwitchOff", 1}, + {"CompartmentLightsSwitchOff", 0}, + {"CompartmentLightsSwitch", 0}, + {"BatterySwitch", 1}, + {"ConverterSwitch", 1}, + {"CompressorSwitch", 1}, + {"CompressorPreset", 1}, + {"CompressorPreset", 99}, + {"CompressorPreset", -1}, + {"MainSwitch", 1}, + {"HeatingSwitch", 1}, + {"HeatingSwitch", 0}, + {"Direction", 1}, + {"Direction", -1}, + {"Direction", 0}, + {"CabActivisation", 1, 1}, + {"CabActivisation", 1, -1}, + {"CabActivisation", 0, 1}, + {"AutoRelaySwitch", 1}, + {"AutoRelaySwitch", 0}, + {"RelayReset", 1}, + {"DoorPermit", 1, 1}, + {"DoorPermit", 2, 1}, + {"DoorPermit", -3, 1}, + {"DoorPermit", 1, -1}, + {"DoorPermit", 3, -1}, + {"DoorOpen", 1, 1}, + {"DoorOpen", 2, -1}, + {"DoorClose", 3, 1}, + {"DoorOpen", 3, 1}, + {"DoorClose", 1, -1}, + {"DoorLock", 0}, + {"DoorLock", 1}, + {"DoorStep", 0}, + {"DoorStep", 1}, + {"DoorMode", 1}, + {"DoorMode", 0}, + {"DepartureSignal", 1}, + {"DepartureSignal", 0}, + {"PantValve", 1}, + {"PantValve", 0x80 | 1}, + {"PantValve", 0x40 | 1, -1}, + {"PantValve", 0x80 | 0x40 | 2, -1}, + {"PantValve", 3}, + {"PantValve", 4}, + {"PantValve", 0x80 | 5}, + {"PantValve", 0x80 | 6}, + {"PantsValve", 1}, + {"PantsValve", 2}, + {"PantsValve", 3}, + {"PantsValve", 4}, + {"PantsValve", 5}, + {"PantsValve", 6}, + {"PantAllDown", 1}, + {"PantAllDown", 0}, + {"MaxCurrentSwitch", 1}, + {"MaxCurrentSwitch", 0}, + {"MinCurrentSwitch", 1}, + {"MinCurrentSwitch", 0}, + {"SetDamage", 4, 1}, + {"SetDamage", 4, -1}, + {"SetDamage", 4, 0}, + {"Emergency_brake", 1}, + {"Emergency_brake", 0}, + {"BrakeDelay", 1}, + {"BrakeDelay", 2}, + {"BrakeDelay", 4}, + {"BrakeDelay", 8}, + {"BrakeDelay", 5}, + {"Sandbox", 1}, + {"Sandbox", 0}, + {"CabSignal", 0}, + {"Load=coal", 5, 0}, + {"Load=Coal", 5, 20}, + {"UnLoad=coal", 5, 0}, + {"UnLoad=Coal", 5, 0}, + {"SpeedCntrl", 40.4}, + {"SpeedCtrlUnit.Parking", 1}, + {"SpeedCtrlUnit.Parking", 0}, + {"SpringBrakeActivate", 1}, + {"SpringBrakeActivate", 0}, + {"MainSwitch", 0}, + {"CompressorSwitch", 0}, + {"ConverterSwitch", 0}, + {"BatterySwitch", 0}, + {"SpringBrakeActivate", 1}, + {"NoSuchCommand", 1}, +}; + +} // namespace + +TEST_CASE("commands: every command run on every real vehicle") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle real{definition::fiz(path), 0.0, "test", 1}; + // loading takes place where the command was given, which comes with the command (PutCommand). a + // vehicle gets none before its first one, TCommand::Location is left uninitialised + real->CommandIn.Location = real->Loc; + auto done{false}; + auto channels{state()}; + channels.emplace_back("Done", [&done](auto const &) { return flag(done); }); + test::trace trace{"commands/" + path, channels, 1, false}; + trace.sample(0.0, real.mover()); + for (std::size_t i = 0; i < commands.size(); ++i) { + auto const &command{commands[i]}; + trace.mark(std::format("{} {} {}", command.name, command.value1, command.value2)); + done = real->RunCommand(command.name, command.value1, command.value2, coupling::control); + trace.sample(static_cast(i + 1), real.mover()); + } + trace.check(); + } +} + +namespace { + +// a switch of the cab, worked on the driven vehicle +struct action { + std::string name; + std::function work; +}; + +// every switch sending its state to the rest of the train, each way and to each range, in the order a crew +// could work them, with the controllers and the direction among them to see what they make of the state +std::vector const actions{ + {"CabActivisation", [](auto &M) { return M.CabActivisation(); }}, + {"BatterySwitch on", [](auto &M) { return M.BatterySwitch(true, range_t::consist); }}, + {"CompartmentLightsSwitch on", [](auto &M) { return M.CompartmentLightsSwitch(true, range_t::consist); }}, + {"CompartmentLightsSwitchOff on", [](auto &M) { return M.CompartmentLightsSwitchOff(true, range_t::consist); }}, + {"CompartmentLightsSwitchOff off", [](auto &M) { return M.CompartmentLightsSwitchOff(false, range_t::unit); }}, + {"WaterPumpBreakerSwitch on", [](auto &M) { return M.WaterPumpBreakerSwitch(true, range_t::consist); }}, + {"WaterPumpSwitch on", [](auto &M) { return M.WaterPumpSwitch(true, range_t::consist); }}, + {"WaterPumpSwitchOff on", [](auto &M) { return M.WaterPumpSwitchOff(true, range_t::unit); }}, + {"WaterPumpSwitchOff off", [](auto &M) { return M.WaterPumpSwitchOff(false, range_t::consist); }}, + {"WaterHeaterBreakerSwitch on", [](auto &M) { return M.WaterHeaterBreakerSwitch(true, range_t::consist); }}, + {"WaterHeaterSwitch on", [](auto &M) { return M.WaterHeaterSwitch(true, range_t::consist); }}, + {"WaterCircuitsLinkSwitch on", [](auto &M) { return M.WaterCircuitsLinkSwitch(true, range_t::consist); }}, + {"FuelPumpSwitch on", [](auto &M) { return M.FuelPumpSwitch(true, range_t::consist); }}, + {"FuelPumpSwitchOff on", [](auto &M) { return M.FuelPumpSwitchOff(true, range_t::unit); }}, + {"FuelPumpSwitchOff off", [](auto &M) { return M.FuelPumpSwitchOff(false, range_t::consist); }}, + {"OilPumpSwitch on", [](auto &M) { return M.OilPumpSwitch(true, range_t::consist); }}, + {"OilPumpSwitchOff on", [](auto &M) { return M.OilPumpSwitchOff(true, range_t::unit); }}, + {"OilPumpSwitchOff off", [](auto &M) { return M.OilPumpSwitchOff(false, range_t::consist); }}, + {"MotorBlowersSwitch front on", [](auto &M) { return M.MotorBlowersSwitch(true, end::front, range_t::consist); }}, + {"MotorBlowersSwitch rear on", [](auto &M) { return M.MotorBlowersSwitch(true, end::rear, range_t::unit); }}, + {"MotorBlowersSwitchOff front on", [](auto &M) { return M.MotorBlowersSwitchOff(true, end::front, range_t::consist); }}, + {"MotorBlowersSwitchOff rear on", [](auto &M) { return M.MotorBlowersSwitchOff(true, end::rear, range_t::unit); }}, + {"MotorBlowersSwitchOff front off", [](auto &M) { return M.MotorBlowersSwitchOff(false, end::front, range_t::consist); }}, + {"MotorBlowersSwitchOff rear off", [](auto &M) { return M.MotorBlowersSwitchOff(false, end::rear, range_t::consist); }}, + {"OperatePantographsValve enable", [](auto &M) { return M.OperatePantographsValve(operation_t::enable, range_t::consist); }}, + {"OperatePantographValve front enable", [](auto &M) { return M.OperatePantographValve(end::front, operation_t::enable, range_t::consist); }}, + {"OperatePantographValve rear enable_on", [](auto &M) { return M.OperatePantographValve(end::rear, operation_t::enable_on, range_t::unit); }}, + {"OperatePantographValve rear enable_off", [](auto &M) { return M.OperatePantographValve(end::rear, operation_t::enable_off, range_t::unit); }}, + {"DirectionForward", [](auto &M) { return M.DirectionForward(); }}, + {"ConverterSwitch on", [](auto &M) { return M.ConverterSwitch(true, range_t::consist); }}, + {"CompressorSwitch on", [](auto &M) { return M.CompressorSwitch(true, range_t::consist); }}, + {"ChangeCompressorPreset 1", [](auto &M) { return M.ChangeCompressorPreset(1, range_t::consist); }}, + {"FuseOn", [](auto &M) { return M.FuseOn(range_t::consist); }}, + {"RelayReset all", [](auto &M) { return M.RelayReset(0xff, range_t::consist); }}, + {"UniversalResetButton 1", [](auto &M) { return M.UniversalResetButton(0, range_t::consist); }}, + {"UniversalResetButton 2", [](auto &M) { return M.UniversalResetButton(1, range_t::unit); }}, + {"MainSwitch on", [](auto &M) { return M.MainSwitch(true, range_t::consist); }}, + {"HeatingSwitch on", [](auto &M) { return M.HeatingSwitch(true, range_t::consist); }}, + {"MaxCurrentSwitch on", [](auto &M) { return M.MaxCurrentSwitch(true, range_t::consist); }}, + {"MinCurrentSwitch on", [](auto &M) { return M.MinCurrentSwitch(true); }}, + {"CurrentSwitch on", [](auto &M) { return M.CurrentSwitch(true); }}, + {"AutoRelaySwitch on", [](auto &M) { return M.AutoRelaySwitch(true); }}, + {"IncMainCtrl 1", [](auto &M) { return M.IncMainCtrl(1); }}, + {"IncMainCtrl 2", [](auto &M) { return M.IncMainCtrl(2); }}, + {"IncScndCtrl 1", [](auto &M) { return M.IncScndCtrl(1); }}, + {"IncScndCtrl 2", [](auto &M) { return M.IncScndCtrl(2); }}, + {"DecScndCtrl 1", [](auto &M) { return M.DecScndCtrl(1); }}, + {"DecScndCtrl 2", [](auto &M) { return M.DecScndCtrl(2); }}, + {"DecMainCtrl 1", [](auto &M) { return M.DecMainCtrl(1); }}, + {"DecMainCtrl 2", [](auto &M) { return M.DecMainCtrl(2); }}, + {"AutoRelaySwitch off", [](auto &M) { return M.AutoRelaySwitch(false); }}, + {"CurrentSwitch off", [](auto &M) { return M.CurrentSwitch(false); }}, + {"MinCurrentSwitch off", [](auto &M) { return M.MinCurrentSwitch(false); }}, + {"MaxCurrentSwitch off", [](auto &M) { return M.MaxCurrentSwitch(false, range_t::unit); }}, + {"DirectionBackward", [](auto &M) { return M.DirectionBackward(); }}, + {"DirectionBackward again", [](auto &M) { return M.DirectionBackward(); }}, + {"DirectionForward", [](auto &M) { return M.DirectionForward(); }}, + {"Sandbox on", [](auto &M) { return M.Sandbox(true, range_t::consist); }}, + {"SandboxManual on", [](auto &M) { return M.SandboxManual(true, range_t::consist); }}, + {"SandboxAuto on", [](auto &M) { return M.SandboxAuto(true, range_t::unit); }}, + {"SandboxAuto off", [](auto &M) { return M.SandboxAuto(false, range_t::consist); }}, + {"SandboxManual off", [](auto &M) { return M.SandboxManual(false, range_t::consist); }}, + {"Sandbox off", [](auto &M) { return M.Sandbox(false, range_t::consist); }}, + {"PermitDoors right", [](auto &M) { return M.PermitDoors(side::right, true, range_t::consist); }}, + {"PermitDoors left", [](auto &M) { return M.PermitDoors(side::left, true, range_t::unit); }}, + {"ChangeDoorPermitPreset +1", [](auto &M) { return M.ChangeDoorPermitPreset(1, range_t::consist); }}, + {"ChangeDoorPermitPreset -1", [](auto &M) { return M.ChangeDoorPermitPreset(-1, range_t::consist); }}, + {"PermitDoorStep off", [](auto &M) { return M.PermitDoorStep(false, range_t::consist); }}, + {"PermitDoorStep on", [](auto &M) { return M.PermitDoorStep(true, range_t::unit); }}, + {"LockDoors off", [](auto &M) { return M.LockDoors(false, range_t::consist); }}, + {"OperateDoors right open local", [](auto &M) { return M.OperateDoors(side::right, true, range_t::local); }}, + {"OperateDoors left open", [](auto &M) { return M.OperateDoors(side::left, true, range_t::consist); }}, + {"OperateDoors right close", [](auto &M) { return M.OperateDoors(side::right, false, range_t::unit); }}, + {"ChangeDoorControlMode remote", [](auto &M) { return M.ChangeDoorControlMode(true, range_t::consist); }}, + {"ChangeDoorControlMode local", [](auto &M) { return M.ChangeDoorControlMode(false, range_t::unit); }}, + {"LockDoors on", [](auto &M) { return M.LockDoors(true, range_t::unit); }}, + {"signal_departure on", [](auto &M) { return M.signal_departure(true, range_t::consist); }}, + {"signal_departure off", [](auto &M) { return M.signal_departure(false, range_t::unit); }}, + {"PermitDoors right off", [](auto &M) { return M.PermitDoors(side::right, false, range_t::consist); }}, + {"HeatingSwitch off", [](auto &M) { return M.HeatingSwitch(false, range_t::unit); }}, + {"MainSwitch off", [](auto &M) { return M.MainSwitch(false, range_t::consist); }}, + {"DropAllPantographs", [](auto &M) { return M.DropAllPantographs(true, range_t::consist); }}, + {"DropAllPantographs released", [](auto &M) { return M.DropAllPantographs(false, range_t::unit); }}, + {"OperatePantographsValve disable", [](auto &M) { return M.OperatePantographsValve(operation_t::disable, range_t::unit); }}, + {"OperatePantographValve front disable_on", [](auto &M) { return M.OperatePantographValve(end::front, operation_t::disable_on, range_t::consist); }}, + {"OperatePantographValve front disable_off", [](auto &M) { return M.OperatePantographValve(end::front, operation_t::disable_off, range_t::consist); }}, + {"OperatePantographValve front none", [](auto &M) { return M.OperatePantographValve(end::front, operation_t::none, range_t::local); }}, + {"CompressorSwitch off", [](auto &M) { return M.CompressorSwitch(false, range_t::unit); }}, + {"ConverterSwitch off", [](auto &M) { return M.ConverterSwitch(false, range_t::unit); }}, + {"WaterCircuitsLinkSwitch off", [](auto &M) { return M.WaterCircuitsLinkSwitch(false, range_t::unit); }}, + {"WaterHeaterSwitch off", [](auto &M) { return M.WaterHeaterSwitch(false, range_t::unit); }}, + {"WaterHeaterBreakerSwitch off", [](auto &M) { return M.WaterHeaterBreakerSwitch(false, range_t::unit); }}, + {"WaterPumpSwitch off", [](auto &M) { return M.WaterPumpSwitch(false, range_t::unit); }}, + {"WaterPumpBreakerSwitch off", [](auto &M) { return M.WaterPumpBreakerSwitch(false, range_t::unit); }}, + {"FuelPumpSwitch off", [](auto &M) { return M.FuelPumpSwitch(false, range_t::unit); }}, + {"OilPumpSwitch off", [](auto &M) { return M.OilPumpSwitch(false, range_t::unit); }}, + {"MotorBlowersSwitch front off", [](auto &M) { return M.MotorBlowersSwitch(false, end::front, range_t::unit); }}, + {"MotorBlowersSwitch rear off", [](auto &M) { return M.MotorBlowersSwitch(false, end::rear, range_t::consist); }}, + {"CompartmentLightsSwitch off", [](auto &M) { return M.CompartmentLightsSwitch(false, range_t::unit); }}, + {"BatterySwitch off", [](auto &M) { return M.BatterySwitch(false, range_t::unit); }}, + {"CabDeactivisation", [](auto &M) { return M.CabDeactivisation(); }}, +}; + +} // namespace + +TEST_CASE("commands: every switch of the cab, sent to the next vehicle") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle driven{definition::fiz(path), 0.0, "driven", 1}; + vehicle next{definition::fiz(path), 0.0, "next"}; + driven->Attach(end::rear, end::front, &next.mover(), test::consist::full_coupling, true, false); + auto done{false}; + auto channels{state()}; + for (auto const &channel : state()) { + channels.emplace_back("next." + channel.name, [&next, value = channel.value](auto const &) { return value(next.mover()); }); + } + channels.emplace_back("Done", [&done](auto const &) { return flag(done); }); + test::trace trace{"switches/" + path, channels, 1, false}; + trace.sample(0.0, driven.mover()); + for (std::size_t i = 0; i < actions.size(); ++i) { + trace.mark(actions[i].name); + done = actions[i].work(driven.mover()); + trace.sample(static_cast(i + 1), driven.mover()); + } + trace.check(); + } +} + +TEST_CASE("commands: every switch of the cab, worked on a pair standing under the catenary") { + // the same switches with time for the vehicles to act on them, as a crew works them: the pantographs rise, + // the main switch closes, the converter and the compressor start, and the controllers take power + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + test::consist pair; + pair.add(definition::fiz(path), 1); + pair.add(definition::fiz(path)); + auto done{false}; + auto channels{state()}; + channels.insert(channels.end(), { + {"PantFrontActive", [](auto const &M) { return flag(M.Pantographs[end::front].is_active); }}, + {"PantRearActive", [](auto const &M) { return flag(M.Pantographs[end::rear].is_active); }}, + {"PantPress", [](auto const &M) { return M.PantPress; }}, + {"GroundRelay", [](auto const &M) { return flag(M.GroundRelay); }}, + {"ConverterFlag", [](auto const &M) { return flag(M.ConverterFlag); }}, + {"CompressorFlag", [](auto const &M) { return flag(M.CompressorFlag); }}, + {"Compressor", [](auto const &M) { return M.Compressor; }}, + {"MainCtrlActualPos", [](auto const &M) { return M.MainCtrlActualPos; }}, + {"Itot", [](auto const &M) { return M.Itot; }}, + {"FuseFlag", [](auto const &M) { return flag(M.FuseFlag); }}, + {"EngineRotation", [](auto const &M) { return M.enrot; }}, + }); + auto const own{channels.size()}; + for (std::size_t i = 0; i < own; ++i) { + channels.emplace_back("next." + channels[i].name, [&pair, value = channels[i].value](auto const &) { return value(pair[1].mover()); }); + } + channels.emplace_back("Done", [&done](auto const &) { return flag(done); }); + test::trace trace{"switches_live/" + path, channels, 1, false}; + trace.sample(0.0, pair[0].mover()); + test::track const catenary{.voltage = test::catenary_3kv}; + for (std::size_t i = 0; i < actions.size(); ++i) { + trace.mark(actions[i].name); + done = actions[i].work(pair[0].mover()); + pair.run(2.0, catenary); + trace.sample(static_cast(i + 1), pair[0].mover()); + } + trace.check(); + } +} + +namespace { + +// three locomotives coupled with the given coupling, the middle one facing backwards, as a train made up of +// whatever came to hand: the commands of the front one go to the rear coupler of the middle one and from its +// front coupler to the front one of the last, which faces backwards from the middle one, so forwards +struct train { + vehicle head{definition::fiz("4e_v1/4e"), 0.0, "front", 1}; + vehicle middle{definition::fiz("4e_v1/4e"), 0.0, "middle"}; + vehicle last{definition::fiz("4e_v1/4e"), 0.0, "last"}; + + explicit train(int Coupling) { + head->Attach(end::rear, end::rear, &middle.mover(), Coupling, true, false); + middle->Attach(end::front, end::front, &last.mover(), Coupling, true, false); + } +}; + +} // namespace + +TEST_CASE("commands: travel along the train, turned round for a vehicle facing backwards") { + train consist{test::consist::full_coupling}; + // the driver takes the front cab, which the others follow + REQUIRE(consist.head->CabActivisation()); + CHECK(consist.head->CabActive == 1); + CHECK(consist.middle->CabActive == -1); + CHECK(consist.last->CabActive == 1); + // the command is done in each vehicle and cleared + CHECK(consist.middle->CommandIn.Command.empty()); + CHECK(consist.last->CommandIn.Command.empty()); + + SUBCASE("door permits: the right side of the sender is the left one of a vehicle facing backwards") { + consist.head->PermitDoors(side::right, true, range_t::consist); + CHECK(consist.head->Doors.instances[side::right].open_permit); + CHECK(consist.middle->Doors.instances[side::left].open_permit); + CHECK_FALSE(consist.middle->Doors.instances[side::right].open_permit); + CHECK(consist.last->Doors.instances[side::right].open_permit); + // and revoked the same way + consist.head->PermitDoors(side::right, false, range_t::consist); + CHECK_FALSE(consist.middle->Doors.instances[side::left].open_permit); + CHECK_FALSE(consist.last->Doors.instances[side::right].open_permit); + } + SUBCASE("a pantograph valve: the vehicles past one coupled by its control cable take the other pantograph") { + consist.head->OperatePantographValve(end::front, operation_t::enable, range_t::consist); + CHECK(consist.head->Pantographs[end::front].valve.is_enabled); + CHECK(consist.middle->Pantographs[end::front].valve.is_enabled); + CHECK(consist.last->Pantographs[end::rear].valve.is_enabled); + } + SUBCASE("sent backwards, it goes no further than the sender: nothing is coupled at its front") { + consist.head->RunCommand("DoorLock", 0, -1, coupling::control); + CHECK_FALSE(consist.head->Doors.lock_enabled); + CHECK(consist.middle->Doors.lock_enabled); + } + SUBCASE("the same command again isn't taken by the next vehicle a second time") { + CHECK(consist.middle->SetInternalCommand("DoorStep", 0, 1, coupling::control)); + CHECK_FALSE(consist.middle->SetInternalCommand("DoorStep", 0, 1, coupling::control)); + CHECK_FALSE(consist.head->RunInternalCommand()); // nothing to run + } +} + +TEST_CASE("commands: don't pass a coupling without the control cable") { + train consist{coupling::coupler | coupling::brakehose}; + consist.head->RunCommand("DoorLock", 0, 1, coupling::control); + CHECK_FALSE(consist.head->Doors.lock_enabled); + CHECK(consist.middle->Doors.lock_enabled); + // a command of another coupling goes through it + consist.head->RunCommand("DoorStep", 0, 1, coupling::brakehose); + CHECK_FALSE(consist.middle->Doors.step_enabled); +} + +TEST_CASE("commands: a wagon loaded and unloaded by commands, which repeat until done") { + vehicle wagon{definition::fiz("3wl_v2/3wl")}; + REQUIRE(wagon->MaxLoad > 0); + test::trace trace{"commands/loading", state(), 100}; + wagon.record(trace); + + // the game gives the commands of loading through PutCommand, with the place of the platform or the ramp + SUBCASE("loading 12 tonnes of coal") { + trace.mark("load"); + wagon->PutCommand("Load=Coal", 12, 0, wagon->Loc); + wagon.run(30.0); + CHECK(wagon->LoadAmount == doctest::Approx(12.0).epsilon(0.1)); + CHECK(wagon->LoadType.name == "coal"); + CHECK(wagon->CommandIn.Command.empty()); + + trace.mark("unload"); + wagon->PutCommand("UnLoad=Coal", 20, 0, wagon->Loc); + wagon.run(200.0); + CHECK(wagon->LoadAmount == doctest::Approx(0.0)); + CHECK(wagon->LoadType.name.empty()); + trace.check(); + } + SUBCASE("a load the wagon doesn't take stays a command, not done") { + wagon->PutCommand("Load=passengers", 12, 0, wagon->Loc); + wagon.run(10.0); + CHECK(wagon->LoadAmount == doctest::Approx(0.0)); + CHECK_FALSE(wagon->CommandIn.Command.empty()); + } + SUBCASE("away from the place of the command nothing is loaded") { + wagon->PutCommand("Load=Coal", 12, 5.0, TLocation{100.0, 0.0, 0.0}); + wagon.run(10.0); + CHECK(wagon->LoadAmount == doctest::Approx(0.0)); + CHECK(wagon->LastLoadChangeTime == doctest::Approx(0.0)); + } + SUBCASE("unloading an empty wagon isn't done either") { + wagon->PutCommand("UnLoad=Coal", 12, 0, wagon->Loc); + wagon.run(10.0); + CHECK_FALSE(wagon->CommandIn.Command.empty()); + } +} diff --git a/tests/unit/test_consist.cpp b/tests/unit/test_consist.cpp new file mode 100644 index 000000000..8305be171 --- /dev/null +++ b/tests/unit/test_consist.cpp @@ -0,0 +1,135 @@ +#include "stdafx.h" +#include "doctest.h" +#include "consist.h" +#include "drive.h" +#include "golden.h" + +#include "McZapkie/hamulce.h" + +using test::consist; +using test::definition; + +// characterisation of trains of coupled vehicles: the real electric units driven whole, a locomotive with +// wagons, the air, the control and the coupler forces between the vehicles, in golden traces +// (data/golden/consist/), each vehicle of the train in its own + +namespace { + +struct unit { + char const *name; + std::vector cars; // from the front, the first one driven +}; + +std::vector const units{ + {"en57", {"en57_v1/5bgaiii_led_spot", "en57_v1/6ba_840", "en57_v1/5bgb"}}, + {"ed72", {"ed72_v1/5bs-ra", "ed72_v1/6bs-sa", "ed72_v1/6bs-sb", "ed72_v1/5bs-rb"}}, + {"ew58", {"ew58_v1/ew58-sa", "ew58_v1/ew58-d", "ew58_v1/ew58-sb"}}, + {"elf", {"elf_v1/21we-a", "elf_v1/21we-b", "elf_v1/21we-c"}}, + {"impuls", {"impuls_v1/31we-a_pr", "impuls_v1/31we-b_pr", "impuls_v1/31we-c_pr", "impuls_v1/31we-d_pr"}}, +}; + +std::vector car_channels() { + using namespace test::channels; + return motion() + traction() + air() + brakes(); +} + +// a trace for each vehicle of the train, recording it +std::vector> record(consist &Train, std::string const &Name) { + std::vector> traces; + for (std::size_t i = 0; i < Train.size(); ++i) { + traces.push_back(std::make_unique(std::format("consist/{}/{}", Name, i), car_channels(), 100)); + Train[i].record(*traces.back()); + } + return traces; +} + +// the events go to every trace +struct all_traces { + std::vector> &traces; + void mark(std::string const &Event) const { + for (auto const &trace : traces) { + trace->mark(Event); + } + } +}; + +} // namespace + +TEST_CASE("consist: every real electric unit driven whole from its front cab") { + for (auto const &unit : units) { + CAPTURE(unit.name); + test::reseed_random(); + consist train; + for (auto const *car : unit.cars) { + train.add(definition::fiz(car), train.size() == 0 ? 1 : 0); + } + auto traces{record(train, unit.name)}; + // the drive cycle marks the events in the trace it gets; the first car's carries them + test::drive_cycle(test::driving(train), test::track{0.15, 0.0, test::catenary_3kv}, *traces.front()); + // every unit runs off and is braked to a stop, but the EN57, which doesn't get ready (COVERAGE.md) + auto const &leading{train[0].mover()}; + CHECK((leading.DistCounter > 0.2 /* km */) != (std::string{unit.name} == "en57")); + CHECK(leading.Vel < 0.5); + for (auto const &trace : traces) { + trace->check(); + } + } +} + +TEST_CASE("consist: a locomotive with wagons, driven, braked by its handle and pulled apart by the couplers") { + test::reseed_random(); + consist train; + train.add(definition::fiz("4e_v1/4e"), 1); + for (auto const *wagon : {"3wl_v2/3wl", "11xa_v2/93a", "bdhpumn_v3/bdhpumn", "z1_v2/152a"}) { + train.add(definition::fiz(wagon)); + } + auto traces{record(train, "eu07 with wagons")}; + test::drive_cycle(test::driving(train), test::track{0.15, 0.0, test::catenary_3kv}, *traces.front()); + for (auto const &trace : traces) { + trace->check(); + } + // the couplers kept the train together + for (std::size_t i = 0; i + 1 < train.size(); ++i) { + CAPTURE(i); + CHECK(std::abs(train.gap(i)) < 0.5); + } +} + +namespace { + +// what the cab's ammeters show (Train.cpp, the driver's panel) and the brake force the AI expects of the +// vehicle (Driver.cpp), read of the driven vehicle +std::vector gauges() { + std::vector result; + for (int ammeter = 0; ammeter < 4; ++ammeter) { + result.emplace_back(std::format("ShowCurrent{}", ammeter), [ammeter](auto const &M) { return M.ShowCurrent(ammeter); }); + } + result.emplace_back("BrakeForceR0.25", [](auto const &M) { return M.BrakeForceR(0.25, M.Vel); }); + result.emplace_back("BrakeForceR1", [](auto const &M) { return M.BrakeForceR(1.0, M.Vel); }); + result.emplace_back("BrakeForceR1at20", [](auto const &M) { return M.BrakeForceR(1.0, 20.0); }); + using test::channels::operator+; + return result + test::channels::traction(); +} + +} // namespace + +TEST_CASE("consist: the gauges of units and locomotives in multiple, driven") { + auto trains{units}; + trains.push_back({"eu07 pair", {"4e_v1/4e", "4e_v1/4e"}}); + trains.push_back({"et22 pair", {"et22_v3/201e-w", "et22_v3/201e-rn"}}); + trains.push_back({"ep09 pair", {"ep09_v2/104e", "ep09_v2/104e"}}); + trains.push_back({"sn61 three", {"sn61_v2/sn61", "sn61_v2/sn61", "sn61_v2/sn61"}}); + trains.push_back({"su45 pair", {"su45_v2/301d", "su45_v2/301d"}}); + for (auto const &train : trains) { + CAPTURE(train.name); + test::reseed_random(); + consist vehicles; + for (auto const *car : train.cars) { + vehicles.add(definition::fiz(car), vehicles.size() == 0 ? 1 : 0); + } + test::trace trace{std::string{"gauges/"} + train.name, gauges(), 100}; + vehicles[0].record(trace); + test::drive_cycle(test::driving(vehicles), test::track{0.15, 0.0, test::catenary_3kv}, trace); + trace.check(); + } +} diff --git a/tests/unit/test_controls.cpp b/tests/unit/test_controls.cpp new file mode 100644 index 000000000..4e5255ff4 --- /dev/null +++ b/tests/unit/test_controls.cpp @@ -0,0 +1,632 @@ +#include "stdafx.h" +#include "doctest.h" +#include "scenario.h" +#include "drive.h" + +#include "McZapkie/hamulce.h" +#include "utilities/utilities.h" + +// characterisation of the controls a driver and a crew use besides driving and braking: doors, direction and +// cab change, sanding against slip, the emergency brake, releasers, the ep and the dynamic brake, heating, +// loading, collisions and the frame of several physics iterations, each in a scenario on real vehicles, +// recorded in golden traces (data/golden/controls/) + +using test::scenario; +using namespace test::channels; + +namespace { + +void notch_up(scenario &Scenario, int Positions, double Seconds) { + for (int i = 0; i < Positions; ++i) { + Scenario.driver().controlling.IncMainCtrl(1); + Scenario.wait(Seconds); + } +} + +void notch_down(scenario &Scenario) { + while (Scenario.driver().controlling.DecMainCtrl(1)) { + Scenario.wait(0.5); + } + Scenario.driver().controlling.DecScndCtrl(2); +} + +void brake_to_stop(scenario &Scenario, double Seconds = 60.0) { + Scenario.act("brake to stop"); + Scenario.cab().BrakeLevelSet(Scenario.cab().Handle->GetPos(bh_FB)); + Scenario.wait(Seconds); + Scenario.cab().BrakeLevelSet(Scenario.cab().Handle->GetPos(bh_RP)); + Scenario.wait(20.0); +} + +// the pipe charged again after an emergency stop: the releaser held with the handle at running, then let go +void recharge(scenario &Scenario) { + auto &cab{Scenario.cab()}; + Scenario.act("releaser held, the pipe charged"); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_RP)); + cab.BrakeReleaser(1); + Scenario.wait(30.0); + cab.BrakeReleaser(0); + Scenario.wait(30.0); +} + +bool stopped(scenario &Scenario) { + for (std::size_t i = 0; i < Scenario.train().size(); ++i) { + if (Scenario.vehicle(i).Vel > 0.1) { + return false; + } + } + return true; +} + +// the brake cylinders of every vehicle of the train under the pressure given +bool released(scenario &Scenario, double Pressure = 0.1) { + for (std::size_t i = 0; i < Scenario.train().size(); ++i) { + if (Scenario.vehicle(i).BrakePress > Pressure) { + return false; + } + } + return true; +} + +bool open(TMoverParameters const &Vehicle, int Side) { + return Vehicle.Doors.instances[Side].is_open; +} + +// the leaf shut, whatever the step does (closed, is_closed, wants the step folded too) +bool shut(TMoverParameters const &Vehicle, int Side) { + return Vehicle.Doors.instances[Side].position <= 0.f; +} + +bool closed(TMoverParameters const &Vehicle, int Side) { + return Vehicle.Doors.instances[Side].is_closed; +} + +} // namespace + +TEST_CASE("controls: the doors of an electric unit, opened, locked, permitted and closed by driving off") { + for (auto const &[name, cars] : std::vector>>{ + {"elf", {"elf_v1/21we-a", "elf_v1/21we-b", "elf_v1/21we-c"}}, + {"en57", {"en57_v1/5bgaiii_led_spot", "en57_v1/6ba_840", "en57_v1/5bgb"}}, + {"impuls", {"impuls_v1/31we-a_pr", "impuls_v1/31we-b_pr", "impuls_v1/31we-c_pr", "impuls_v1/31we-d_pr"}}}) { + CAPTURE(name); + scenario scenario{"controls/doors/" + name, motion() + doors()}; + for (auto const *car : cars) { + scenario.add(car, scenario.train().size() == 0 ? 1 : 0); + } + // the EN57 doesn't get ready (COVERAGE.md), its doors work all the same + auto const ready{scenario.start_up()}; + CHECK(ready == (name != "en57")); + auto &cab{scenario.cab()}; + auto const &last{scenario.vehicle(scenario.train().size() - 1)}; + scenario.act("permit left"); + cab.PermitDoors(side::left, true); + scenario.wait(2.0); + scenario.act("open left"); + cab.OperateDoors(side::left, true); + scenario.wait(10.0); + CHECK(open(cab, side::left)); + CHECK(open(last, side::left)); + CHECK(closed(cab, side::right)); + scenario.act("close left"); + cab.OperateDoors(side::left, false); + scenario.wait(10.0); + CHECK(shut(cab, side::left)); + CHECK(shut(last, side::left)); + scenario.act("open right without permit"); + cab.OperateDoors(side::right, true); + scenario.wait(5.0); + // the EN57 needs no permit + CHECK(shut(cab, side::right) == cab.Doors.permit_needed); + scenario.act("permit right, open"); + cab.PermitDoors(side::right, true); + cab.OperateDoors(side::right, true); + scenario.wait(10.0); + CHECK(open(cab, side::right)); + CHECK(open(last, side::right)); + scenario.act("lock"); + cab.LockDoors(true); + scenario.wait(5.0); + scenario.act("permit preset, steps off, remote control"); + cab.ChangeDoorPermitPreset(1); + cab.PermitDoorStep(false); + cab.ChangeDoorControlMode(true); + scenario.wait(5.0); + cab.ChangeDoorPermitPreset(-1); + cab.PermitDoorStep(true); + cab.ChangeDoorControlMode(false); + scenario.act("close all, unlock"); + cab.OperateDoors(side::right, false); + cab.OperateDoors(side::left, false); + cab.LockDoors(false); + scenario.wait(10.0); + CHECK(shut(cab, side::left)); + CHECK(shut(cab, side::right)); + scenario.act("open both and drive off"); + cab.PermitDoors(side::left, true); + cab.PermitDoors(side::right, true); + cab.OperateDoors(side::left, true); + cab.OperateDoors(side::right, true); + scenario.wait(5.0); + CHECK_FALSE(shut(cab, side::left)); + CHECK_FALSE(shut(cab, side::right)); + scenario.get_going(); + notch_up(scenario, 4, 2.0); + scenario.wait(20.0); + // the doors of a train ready to run close as the driver starts off. no CHECK of the speed: the Elf and + // the Impuls keep their steps out under the permit, which keeps them from moving (COVERAGE.md) + CHECK(shut(cab, side::left) == ready); + CHECK(shut(cab, side::right) == ready); + CHECK(shut(last, side::left) == ready); + CHECK(shut(last, side::right) == ready); + notch_down(scenario); + brake_to_stop(scenario); + scenario.check(); + } +} + +TEST_CASE("controls: the doors of wagons, worked from the locomotive") { + scenario scenario{"controls/doors/eu07 with wagons", motion() + doors()}; + scenario.add("4e_v1/4e", 1).add("bdhpumn_v3/bdhpumn").add("11xa_v2/93a"); + scenario.start_up(); + auto &cab{scenario.cab()}; + auto &coach{scenario.vehicle(1)}; + auto const &freight{scenario.vehicle(2)}; + // the locomotive works the coach's doors for the consist and for the unit, not with its local controls; the + // freight wagon's doors don't move from the locomotive + for (auto const range : {range_t::consist, range_t::unit, range_t::local}) { + CAPTURE(static_cast(range)); + scenario.act(std::format("open right {}", static_cast(range))); + cab.PermitDoors(side::right, true, range); + cab.OperateDoors(side::right, true, range); + scenario.wait(10.0); + CHECK(open(coach, side::right) == (range != range_t::local)); + CHECK(shut(coach, side::left)); + CHECK(shut(freight, side::right)); + scenario.act("close right"); + cab.OperateDoors(side::right, false, range); + scenario.wait(10.0); + CHECK(shut(coach, side::right)); + } + scenario.act("wagon opens its own"); + coach.OperateDoors(side::left, true, range_t::local); + // a second at a time, the same run as one wait. the coach's doors close by themselves 5 s after they + // opened (DoorStayOpen) + scenario.wait(1.0); + CHECK_FALSE(shut(coach, side::left)); + scenario.wait(9.0); + CHECK(shut(coach, side::left)); + coach.OperateDoors(side::left, false, range_t::local); + scenario.wait(10.0); + CHECK(shut(coach, side::left)); + scenario.check(); +} + +TEST_CASE("controls: reversing and changing the cab of a locomotive") { + scenario scenario{"controls/reverse/eu07", motion() + traction()}; + scenario.add("4e_v1/4e", 1); + scenario.start_up(); + scenario.get_going(); + auto &cab{scenario.cab()}; + // the speed along the vehicle, its front ahead + notch_up(scenario, 10, 1.5); + scenario.wait(10.0); + CHECK(cab.V > 3.0); + notch_down(scenario); + brake_to_stop(scenario); + CHECK(stopped(scenario)); + scenario.act("backward"); + cab.DirectionBackward(); + cab.DirectionBackward(); + scenario.wait(2.0); + CHECK(cab.DirActive < 0); + notch_up(scenario, 10, 1.5); + scenario.wait(10.0); + CHECK(cab.V < -3.0); + notch_down(scenario); + brake_to_stop(scenario); + CHECK(stopped(scenario)); + scenario.act("neutral, change cab"); + cab.DirectionForward(); + scenario.wait(1.0); + cab.CabDeactivisation(); + // through the engine room (cab 0) to the rear cab, as the player walks there + cab.ChangeCab(-1); + cab.ChangeCab(-1); + cab.CabActivisation(); + scenario.wait(5.0); + CHECK(cab.CabActive < 0); + scenario.act("drive from the rear cab"); + scenario.get_going(); + notch_up(scenario, 10, 1.5); + scenario.wait(10.0); + // forward from the rear cab is the vehicle's backward + CHECK(cab.V < -3.0); + notch_down(scenario); + brake_to_stop(scenario); + CHECK(stopped(scenario)); + scenario.check(); +} + +TEST_CASE("controls: wheel slip on a slippery track, sand and the anti-slip brake") { + for (auto const *path : {"4e_v1/4e", "sm42_v1/6d1", "e186_v2/eu47"}) { + CAPTURE(path); + scenario scenario{std::string{"controls/slip/"} + path, motion() + traction() + brakes(), test::track{0.04, 0.0, test::catenary_3kv}}; + scenario.add(path, 1); + scenario.start_up(); + scenario.get_going(); + auto &cab{scenario.cab()}; + // a notch a second: the controller put to full power at once never gets the line contactors closed + scenario.act("full power"); + scenario.full_power(0.0); + auto slipped{cab.SlippingWheels}; + for (int i = 0; i < 10; ++i) { + scenario.wait(1.0); + slipped = slipped || cab.SlippingWheels; + } + // the electric locomotives slip at full power, the SM42, of less power for its weight, doesn't + CHECK(slipped == (std::string{path} != "sm42_v1/6d1")); + scenario.act("sand"); + cab.Sandbox(true); + scenario.wait(10.0); + cab.Sandbox(false); + scenario.act("manual sand"); + cab.SandboxManual(true); + scenario.wait(5.0); + cab.SandboxManual(false); + scenario.act("anti-slip brake"); + cab.AntiSlippingButton(); + cab.AntiSlippingBrake(); + scenario.wait(10.0); + CHECK(cab.Vel > 5.0); + notch_down(scenario); + brake_to_stop(scenario); + CHECK(stopped(scenario)); + scenario.check(); + } +} + +TEST_CASE("controls: emergency stops by the alarm chain and the radio-stop") { + scenario scenario{"controls/emergency/eu07 with wagons", motion() + brakes()}; + scenario.add("4e_v1/4e", 1).add("bdhpumn_v3/bdhpumn").add("3wl_v2/3wl"); + scenario.start_up(); + scenario.get_going(); + notch_up(scenario, 20, 1.0); + scenario.wait(20.0); + notch_down(scenario); + auto &cab{scenario.cab()}; + CHECK(cab.Vel > 30.0); + scenario.act("alarm chain in the wagon"); + scenario.vehicle(1).AlarmChainSwitch(true); + scenario.wait(40.0); + // the pipe emptied, the whole train braked to a stop + CHECK(stopped(scenario)); + CHECK(cab.PipePress < 0.5); + CHECK(scenario.vehicle(2).BrakePress > 1.0); + scenario.act("alarm chain reset"); + scenario.vehicle(1).AlarmChainSwitch(false); + scenario.wait(10.0); + // the FV4a handle stays cut off the emptied pipe of an LSt locomotive (lock_old in UpdatePipePressure) + // until the releaser is held + CHECK(cab.PipePress < 0.5); + recharge(scenario); + CHECK(cab.PipePress > 4.5); + scenario.get_going(); + notch_up(scenario, 20, 1.0); + scenario.wait(20.0); + notch_down(scenario); + CHECK(cab.Vel > 30.0); + scenario.act("radio-stop"); + cab.RadiostopSwitch(true); + scenario.wait(40.0); + CHECK(stopped(scenario)); + CHECK(cab.PipePress < 0.5); + scenario.act("radio-stop reset"); + cab.RadiostopSwitch(false); + scenario.wait(10.0); + recharge(scenario); + CHECK(cab.PipePress > 4.5); + scenario.check(); +} + +TEST_CASE("controls: releasers and the universal brake buttons") { + scenario scenario{"controls/releaser/eu07 with wagons", brakes() + air()}; + scenario.add("4e_v1/4e", 1).add("bdhpumn_v3/bdhpumn").add("3wl_v2/3wl"); + scenario.start_up(); + scenario.get_going(); + auto &cab{scenario.cab()}; + auto &wagon{scenario.vehicle(2)}; + scenario.act("service brake"); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_FB)); + scenario.wait(20.0); + CHECK(cab.BrakePress > 1.0); + CHECK(wagon.BrakePress > 1.0); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_NP)); + auto &coach{scenario.vehicle(1)}; + // the releaser of the cab is passed along the whole train, a wagon's works on the wagon alone + auto const coachbraked{coach.BrakePress}; + auto wagonbraked{wagon.BrakePress}; + scenario.act("releaser of the locomotive"); + cab.BrakeReleaser(1); + scenario.wait(10.0); + cab.BrakeReleaser(0); + CHECK(cab.BrakePress < 0.5); + CHECK(coach.BrakePress < coachbraked - 1.0); + CHECK(wagon.BrakePress < wagonbraked - 0.5); + CHECK(cab.PipePress < 4.0); // the pipe stays where the service application left it + wagonbraked = wagon.BrakePress; + scenario.act("releaser of the wagon"); + wagon.BrakeReleaser(1); + scenario.wait(10.0); + wagon.BrakeReleaser(0); + CHECK(wagon.BrakePress < wagonbraked - 0.5); + for (int button = 0; button < 3; ++button) { + scenario.act(std::format("universal button {}", button)); + cab.UniversalBrakeButton(button, 1); + scenario.wait(5.0); + cab.UniversalBrakeButton(button, 0); + scenario.wait(5.0); + } + scenario.act("running"); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_RP)); + scenario.wait(60.0); + CHECK(cab.PipePress > 4.8); + CHECK(released(scenario)); + scenario.check(); +} + +TEST_CASE("controls: the ep brake of electric units") { + for (auto const &[name, cars] : std::vector>>{ + {"ed72", {"ed72_v1/5bs-ra", "ed72_v1/6bs-sa", "ed72_v1/6bs-sb", "ed72_v1/5bs-rb"}}, + {"ew58", {"ew58_v1/ew58-sa", "ew58_v1/ew58-d", "ew58_v1/ew58-sb"}}}) { + CAPTURE(name); + scenario scenario{"controls/ep brake/" + name, motion() + brakes() + handles()}; + for (auto const *car : cars) { + scenario.add(car, scenario.train().size() == 0 ? 1 : 0); + } + scenario.start_up(); + scenario.get_going(); + auto &cab{scenario.cab()}; + scenario.act("ep on"); + cab.SwitchEPBrake(1); + scenario.wait(5.0); + auto const &last{scenario.vehicle(scenario.train().size() - 1)}; + auto const positions{std::vector{bh_EPB, bh_EPN, bh_EPR, bh_EPB, bh_RP}}; + for (std::size_t i = 0; i < positions.size(); ++i) { + CAPTURE(positions[i]); + scenario.act(std::format("handle {}", positions[i])); + cab.BrakeLevelSet(cab.Handle->GetPos(positions[i])); + scenario.wait(15.0); + if (i == 2) { + CHECK(released(scenario)); + } + if (i == 3) { + // the ep brake works the cylinders of every car by wire, the pipe stays charged (by the first + // application the EW58's pipe is still charging after the start-up) + CHECK(cab.PipePress > 4.8); + CHECK(cab.BrakePress > 1.0); + CHECK(last.BrakePress > 1.0); + } + } + // the EW58 releases at the running position, the ED72 holds the ep application there, and with the ep + // brake switched off as well: only the ep release position lets it off + auto const ed72{name == "ed72"}; + CHECK(released(scenario) != ed72); + scenario.act("ep off"); + cab.SwitchEPBrake(0); + scenario.wait(30.0); + CHECK(released(scenario) != ed72); + scenario.check(); + } +} + +TEST_CASE("controls: the electrodynamic brake of locomotives having one") { + for (auto const *path : {"ep09_v2/104e", "e186_v2/eu47", "e6act_v2/e6acta", "et22_v3/201e-rn"}) { + CAPTURE(path); + scenario scenario{std::string{"controls/dynamic brake/"} + path, motion() + traction() + brakes()}; + scenario.add(path, 1).add("bdhpumn_v3/bdhpumn").add("bdhpumn_v3/bdhpumn"); + scenario.start_up(); + scenario.get_going(); + notch_up(scenario, 60, 1.0); + scenario.wait(30.0); + notch_down(scenario); + auto &cab{scenario.cab()}; + CHECK(cab.Vel > 40.0); + auto const speed{cab.Vel}; + scenario.act("dynamic brake on"); + cab.DynamicBrakeSwitch(true); + scenario.wait(5.0); + scenario.act("brake"); + cab.BrakeLevelSet(cab.Handle->GetPos(bh_FB)); + scenario.wait(40.0); + CHECK(cab.Vel < speed); + scenario.act("dynamic brake off"); + cab.DynamicBrakeSwitch(false); + brake_to_stop(scenario, 30.0); + CHECK(stopped(scenario)); + scenario.check(); + } +} + +TEST_CASE("controls: train heating from the locomotive") { + scenario scenario{"controls/heating/eu07 with wagons", traction() + air()}; + scenario.add("4e_v1/4e", 1).add("bdhpumn_v3/bdhpumn").add("bdhpumn_v3/bdhpumn"); + scenario.start_up(); + auto &cab{scenario.cab()}; + scenario.act("heating on"); + cab.HeatingSwitch(true); + scenario.wait(30.0); + CHECK(cab.Heating); + CHECK(scenario.vehicle(2).Heating); + scenario.act("heating off"); + cab.HeatingSwitch(false); + scenario.wait(10.0); + CHECK_FALSE(cab.Heating); + CHECK_FALSE(scenario.vehicle(2).Heating); + scenario.check(); +} + +TEST_CASE("controls: a freight wagon loaded and braked") { + auto emptymass{0.0}; + for (auto const *load : {"", "coal", "ruda", "unknown"}) { + CAPTURE(load); + scenario scenario{std::string{"controls/load/3wl "} + (*load ? load : "empty"), motion() + brakes()}; + scenario.add("4e_v1/4e", 1).add("3wl_v2/3wl"); + auto &wagon{scenario.vehicle(1)}; + auto const known{std::string{load} == "coal" || std::string{load} == "ruda"}; + CHECK(wagon.AssignLoad(load, 30.f) == (known || std::string{load}.empty())); + wagon.ComputeMass(); + if (*load == '\0') { + emptymass = wagon.TotalMass; + } + else { + // 30 t of a load the wagon takes, none of one it doesn't + CHECK(wagon.TotalMass == doctest::Approx(emptymass + (known ? 30000.0 : 0.0)).epsilon(0.01)); + } + scenario.start_up(); + scenario.get_going(); + notch_up(scenario, 20, 1.0); + scenario.wait(20.0); + notch_down(scenario); + CHECK(scenario.cab().Vel > 20.0); + brake_to_stop(scenario); + CHECK(stopped(scenario)); + scenario.check(); + } +} + +TEST_CASE("controls: a locomotive running into a standing wagon") { + for (auto const speed : {5.0, 15.0, 40.0}) { + CAPTURE(speed); + scenario scenario{std::format("controls/collision/{} kmh", static_cast(speed)), motion()}; + // the wagon stands in front, the locomotive comes from behind it + scenario.add("bdhpumn_v3/bdhpumn").add("4e_v1/4e", 0, coupling::faux, speed, 30.0); + auto const &wagon{scenario.vehicle(0)}; + scenario.wait(30.0); + // slow, the locomotive pushes the wagon off, the buffers only touching, and both roll to a stop; fast, + // the wagon derails where it stands + auto const derailed{speed > 30.0}; + CHECK(TestFlag(wagon.DamageFlag, dtrain_out) == derailed); + CHECK((std::abs(wagon.DistCounter) > 0.0001) != derailed); + CHECK(stopped(scenario)); + scenario.check(); + } +} + +TEST_CASE("controls: a train run with five physics steps in a frame") { + scenario scenario{"controls/full physics/eu07 with wagons", motion() + traction() + brakes(), + test::track{0.15, 0.0, test::catenary_3kv, 0, 5}}; + scenario.add("4e_v1/4e", 1).add("bdhpumn_v3/bdhpumn").add("3wl_v2/3wl"); + scenario.start_up(); + scenario.get_going(); + notch_up(scenario, 25, 1.0); + scenario.wait(30.0); + notch_down(scenario); + // no CHECKs of the run: with five steps a frame the motors take no current and the train rolls at a few + // km/h, which is a finding (COVERAGE.md), not the behaviour to keep + brake_to_stop(scenario); + scenario.check(); +} + +TEST_CASE("controls: the automatic electrodynamic brake of the EP09, in service braking held to a stop") { + // held for longer, a service application of the train brake makes the EP09 brake electrodynamically: the + // pressure its distributor asks for (GetEDBCP) brings the motors in as generators, the air of its own + // cylinders is let out, and given back as the train slows down and the motors' brake force fades + struct application { + char const *name; + double steps; // past the first braking position of the handle + }; + for (auto const &[name, steps] : {application{"light", 0.0}, application{"medium", 1.0}, application{"high", 2.0}, application{"full service", 3.0}}) { + CAPTURE(name); + scenario scenario{std::string{"controls/dynamic brake held/ep09 "} + name, motion() + traction() + brakes()}; + scenario.add("ep09_v2/104e", 1, test::consist::full_coupling, 120.0); + scenario.add("bdhpumn_v3/bdhpumn", 0, test::consist::full_coupling, 120.0); + scenario.add("bdhpumn_v3/bdhpumn", 0, test::consist::full_coupling, 120.0); + REQUIRE(scenario.start_up()); + auto &ep09{scenario.cab()}; + REQUIRE(ep09.Power110vIsAvailable); + scenario.act(std::string{"brake "} + name); + ep09.BrakeLevelSet(ep09.Handle->GetPos(bh_MB) + steps); + scenario.wait(10.0); + CHECK(ep09.DynamicBrakeFlag); + CHECK(ep09.Im < -100.0); + CHECK(ep09.BrakePress < 0.25 * ep09.Hamulec->GetEDBCP()); + scenario.wait(110.0); + // stopped, held by the air alone. the flag of the electrodynamic brake stays on while the cylinders hold + // less than 2.1 bar, though the motors give nothing; a stronger application drops it before the stop + CHECK(ep09.Vel == doctest::Approx(0.0)); + CHECK(ep09.Im == doctest::Approx(0.0)); + CHECK(ep09.DynamicBrakeFlag == (ep09.BrakePress <= 2.1)); + CHECK(ep09.BrakePress == doctest::Approx(ep09.Hamulec->GetEDBCP()).epsilon(0.05)); + scenario.act("release"); + ep09.BrakeLevelSet(ep09.Handle->GetPos(bh_RP)); + scenario.wait(30.0); + scenario.check(); + } +} + +TEST_CASE("controls: the automatic electrodynamic brake under ep and service braking, from speed") { + // ep, light and full service braking of trains running at speed, whose locomotive or unit has the automatic + // electrodynamic brake + struct train { + char const *name; + std::vector cars; // the first one driven + double speed; // km/h at the start + bool epelectric; // whether the ep brake alone brings the motors in + }; + // the EW58's motors brake by the stages of the ep brake (TUHEX), with the 110 V of the converter in its + // middle car; the E6ACT's by its distributor, from a deeper service application + for (auto const &[name, cars, speed, epelectric] : { + train{"e6act with wagons", {"e6act_v2/e6acta", "bdhpumn_v3/bdhpumn", "bdhpumn_v3/bdhpumn"}, 100.0, false}, + train{"ew58", {"ew58_v1/ew58-sa", "ew58_v1/ew58-d", "ew58_v1/ew58-sb"}, 90.0, true}, + }) { + CAPTURE(name); + scenario scenario{std::string{"controls/dynamic brake from speed/"} + name, motion() + traction() + brakes()}; + for (auto const *car : cars) { + scenario.add(car, car == cars.front() ? 1 : 0, test::consist::full_coupling, speed); + } + scenario.start_up(); + auto &cab{scenario.cab()}; + auto const brake{[&scenario, &cab](char const *Event, double Position, double Seconds) { + scenario.act(Event); + cab.BrakeLevelSet(Position); + scenario.wait(Seconds); + }}; + brake("ep brake", cab.Handle->GetPos(bh_EPB), 15.0); + CHECK(cab.DynamicBrakeFlag == epelectric); + CHECK((cab.Im < -100.0) == epelectric); + brake("ep hold", cab.Handle->GetPos(bh_EPN), 5.0); + brake("first step", cab.Handle->GetPos(bh_MB), 20.0); + brake("deeper", cab.Handle->GetPos(bh_MB) + 1.0, 15.0); + // the pipe alone doesn't bring the EW58's motors in + CHECK((cab.Im < -50.0) != epelectric); + brake("release", cab.Handle->GetPos(bh_RP), 20.0); + brake("full service", cab.Handle->GetPos(bh_FB), 40.0); + brake("release", cab.Handle->GetPos(bh_RP), 20.0); + scenario.check(); + } +} + +TEST_CASE("controls: a draisine pumped along by hand") { + // the pumping force, half a stroke forwards and half backwards, is below the resistance of a draisine + // standing, which stays where it is; a rolling one is kept going + for (auto const speed : {0.0, 10.0}) { + CAPTURE(speed); + scenario scenario{std::format("controls/draisine/{}", static_cast(speed)), motion() + traction()}; + scenario.add("dr2_v1/dr2", 1, test::consist::full_coupling, speed); + auto &cab{scenario.cab()}; + cab.CabActivisation(); + scenario.act("hand brake off"); + test::release_brakes(cab); + for (int stroke = 0; stroke < 40; ++stroke) { + scenario.act("stroke"); + cab.IncMainCtrl(1); + scenario.wait(0.5); + cab.DecMainCtrl(1); + scenario.wait(0.5); + } + CHECK((speed > 0.0) == (cab.Vel > 1.0)); + scenario.act("coast"); + scenario.wait(20.0); + scenario.check(); + } +} diff --git a/tests/unit/test_couplers_trains.cpp b/tests/unit/test_couplers_trains.cpp new file mode 100644 index 000000000..926964dbe --- /dev/null +++ b/tests/unit/test_couplers_trains.cpp @@ -0,0 +1,174 @@ +#include "stdafx.h" +#include "doctest.h" +#include "consist.h" +#include "scenario.h" + +#include "utilities/utilities.h" + +// characterisation of the couplers between vehicles: coupling and uncoupling by hand (Attach, DettachStatus, +// Dettach), the automatic couplers coupling on contact, a broken coupler, and collisions which derail, on real +// vehicles, the moving ones recorded in golden traces (data/golden/couplers/) + +using test::consist; +using test::definition; +using test::scenario; +using namespace test::channels; + +TEST_CASE("couplers: coupled by hand, touching or apart, of the same kind or not") { + consist train; + auto &locomotive{train.add(definition::fiz("4e_v1/4e")).mover()}; + // the wagon's buffers touch the locomotive's, the unit's car stands a metre away + auto &wagon{train.add(definition::fiz("3wl_v2/3wl"), 0, coupling::faux).mover()}; + train.step(); + + // nothing to couple to, or nothing to couple + CHECK_FALSE(locomotive.Attach(end::rear, end::front, nullptr, coupling::coupler)); + CHECK_FALSE(locomotive.Attach(end::rear, end::front, &wagon, coupling::faux)); + // the hook first: the coupler alone, which sounds + locomotive.Couplers[end::rear].sounds = 0; + REQUIRE(locomotive.Attach(end::rear, end::front, &wagon, coupling::coupler)); + CHECK(locomotive.Couplers[end::rear].Connected == &wagon); + CHECK(wagon.Couplers[end::front].Connected == &locomotive); + CHECK(wagon.Couplers[end::front].CouplingFlag == coupling::coupler); + CHECK(locomotive.Couplers[end::rear].sounds == sound::attachcoupler); + // then the hoses: only what changes sounds + locomotive.Couplers[end::rear].sounds = 0; + REQUIRE(locomotive.Attach(end::rear, end::front, &wagon, coupling::coupler | coupling::brakehose | coupling::mainhose)); + CHECK(locomotive.Couplers[end::rear].sounds == (sound::attachbrakehose | sound::attachmainhose)); + // and again, quietly, the other side naming the coupler by the one it's connected to (2) + locomotive.Couplers[end::rear].sounds = 0; + REQUIRE(wagon.Attach(end::front, 2, &locomotive, coupling::coupler | coupling::brakehose, false, false)); + CHECK(locomotive.Couplers[end::rear].CouplingFlag == (coupling::coupler | coupling::brakehose)); + CHECK(locomotive.Couplers[end::rear].sounds == 0); + + // a screw coupler with the buffers pressed together comes apart whole + train.step(); + CHECK(locomotive.DettachStatus(end::rear) < 0); + locomotive.Couplers[end::rear].sounds = 0; + CHECK(locomotive.Dettach(end::rear)); + CHECK(locomotive.Couplers[end::rear].Connected == nullptr); + CHECK(wagon.Couplers[end::front].Connected == nullptr); + CHECK(locomotive.Couplers[end::rear].sounds == (sound::detach | sound::attachcoupler | sound::attachbrakehose)); + CHECK(locomotive.DettachStatus(end::rear) == 0); + CHECK(locomotive.Dettach(end::rear)); +} + +TEST_CASE("couplers: a screw coupler uncoupled with the buffers apart, or with the hoses alone, or broken") { + struct state { + char const *name; + double gap; // between the buffers, the coupler stretched + int coupling; + bool damaged; + bool apart; // whether it comes apart whole + }; + for (auto const &[name, gap, couplingtype, damaged, apart] : { + state{"stretched a little", 0.1, coupling::coupler | coupling::brakehose, false, false}, + state{"stretched far", 0.3, coupling::coupler | coupling::brakehose, false, true}, + state{"hoses alone", 0.1, coupling::brakehose, false, true}, + state{"broken", 0.1, coupling::coupler | coupling::brakehose, true, true}, + }) { + CAPTURE(name); + consist train; + auto &locomotive{train.add(definition::fiz("4e_v1/4e")).mover()}; + auto &wagon{train.add(definition::fiz("3wl_v2/3wl"), 0, coupling::faux, 0.0, gap).mover()}; + REQUIRE(locomotive.Attach(end::rear, end::front, &wagon, couplingtype, true)); + if (damaged) { + SetFlag(locomotive.DamageFlag, dtrain_coupling); + } + train.step(); + CHECK((locomotive.DettachStatus(end::rear) < 0) == apart); + CHECK(locomotive.Dettach(end::rear) == apart); + // what's left when it doesn't come apart is the coupler, the hoses taken off + CHECK(locomotive.Couplers[end::rear].CouplingFlag == (apart ? coupling::faux : coupling::coupler)); + CHECK(wagon.Couplers[end::front].CouplingFlag == (apart ? coupling::faux : coupling::coupler)); + } + // couplers of different kinds don't couple, unless forced + consist train; + auto &locomotive{train.add(definition::fiz("4e_v1/4e")).mover()}; + auto &unit{train.add(definition::fiz("en57_v1/5bgb"), 0, coupling::faux).mover()}; + train.step(); + CHECK_FALSE(locomotive.Attach(end::rear, end::front, &unit, coupling::coupler)); + CHECK(locomotive.Attach(end::rear, end::front, &unit, coupling::coupler, true)); +} + +TEST_CASE("couplers: automatic couplers coupling on contact") { + // a car running slowly into another from close by (its brakes are on), the automatic couplers of both ends + // set to couple what their AutomaticFlag says; without one, the hook alone + struct meeting { + char const *name; + char const *standing; // its rear coupler met + char const *running; // its front coupler meets + int coupling; // what they couple: what both flags allow, the control cable only of one kind + }; + for (auto const &[name, standing, running, couplingtype] : { + meeting{"en57 to en57", "en57_v1/5bgb", "en57_v1/5bgaiii_led_spot", 39}, + meeting{"impuls to en57", "en57_v1/5bgb", "impuls_v1/31we-a_pr", 39 & ~coupling::control}, + meeting{"en57 without the flag", "en57_v1/5bgaiii_led_spot", "en57_v1/6ba_840", coupling::coupler}, + }) { + CAPTURE(name); + scenario scenario{std::string{"couplers/automatic/"} + name, motion(), test::track{}}; + scenario.add(standing, 0, consist::full_coupling); + scenario.add(running, 0, coupling::faux, 3.0, 0.2); + scenario.wait(10.0); + auto &first{scenario.vehicle(0)}; + auto &second{scenario.vehicle(1)}; + CHECK(first.Couplers[end::rear].CouplingFlag == couplingtype); + CHECK(second.Couplers[end::front].CouplingFlag == couplingtype); + // once coupled, not again until uncoupled; the vehicle which coupled marks it for the AI + CHECK_FALSE(first.Couplers[end::rear].AutomaticCouplingAllowed); + CHECK_FALSE(second.Couplers[end::front].AutomaticCouplingAllowed); + CHECK((TestFlag(first.AIFlag, sound::attachcoupler) || TestFlag(second.AIFlag, sound::attachcoupler))); + scenario.check(); + } +} + +TEST_CASE("couplers: a coupler broken, with its brake hose torn off") { + consist train; + auto &locomotive{train.add(definition::fiz("4e_v1/4e")).mover()}; + auto &wagon{train.add(definition::fiz("3wl_v2/3wl")).mover()}; + REQUIRE(locomotive.Couplers[end::rear].Connected == &wagon); + locomotive.damage_coupler(end::rear); + CHECK(TestFlag(locomotive.DamageFlag, dtrain_coupling)); + CHECK(locomotive.AlarmChainFlag); + CHECK(locomotive.Couplers[end::rear].CouplingFlag == coupling::faux); + CHECK(wagon.Couplers[end::front].CouplingFlag == coupling::faux); + // the front coupler of the wagon, broken on its own: no hose to tear + wagon.damage_coupler(end::front); + CHECK(TestFlag(wagon.DamageFlag, dtrain_coupling)); + CHECK_FALSE(wagon.AlarmChainFlag); + // articulated couplers don't break + consist articulated; + auto &unit{articulated.add(definition::fiz("impuls_v1/31we-a_pr")).mover()}; + articulated.add(definition::fiz("impuls_v1/31we-b_pr")); + unit.damage_coupler(end::rear); + CHECK_FALSE(TestFlag(unit.DamageFlag, dtrain_coupling)); + CHECK(unit.Couplers[end::rear].CouplingFlag != coupling::faux); +} + +TEST_CASE("couplers: collisions fast enough to derail") { + // a locomotive running into a standing wagon from behind, and a wagon running backwards into a standing + // locomotive; over 15 km/h, weighed by the ratio of the masses, the vehicles derail + struct collision { + char const *name; + char const *frontpath; + double frontspeed; + char const *back; + double backspeed; + bool frontout; + bool backout; + }; + for (auto const &[name, frontpath, frontspeed, back, backspeed, frontout, backout] : { + collision{"locomotive into a wagon at 60", "3wl_v2/3wl", 0.0, "4e_v1/4e", 60.0, true, false}, + collision{"wagon backwards into a locomotive at 60", "3wl_v2/3wl", -60.0, "4e_v1/4e", 0.0, true, false}, + collision{"locomotive into a locomotive at 25", "4e_v1/4e", 0.0, "4e_v1/4e", 25.0, true, true}, + }) { + CAPTURE(name); + scenario scenario{std::string{"couplers/collision/"} + name, motion(), test::track{}}; + scenario.add(frontpath, 0, consist::full_coupling, frontspeed); + scenario.add(back, 0, coupling::faux, backspeed, 20.0); + scenario.wait(10.0); + CHECK(TestFlag(scenario.vehicle(0).DamageFlag, dtrain_out) == frontout); + CHECK(TestFlag(scenario.vehicle(1).DamageFlag, dtrain_out) == backout); + scenario.check(); + } +} diff --git a/tests/unit/test_definition.cpp b/tests/unit/test_definition.cpp index 6b2d446c0..6b0ce22b3 100644 --- a/tests/unit/test_definition.cpp +++ b/tests/unit/test_definition.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "doctest.h" #include "vehicle.h" +#include "drive.h" using test::definition; using test::vehicle; @@ -43,3 +44,108 @@ TEST_CASE("definition: rows of a table section reach the vehicle") { CHECK(wagon->LightsPos == 1); CHECK(wagon->Lights[end::front][1] == pc1); } + +TEST_CASE("definition: lines the loader skips or trims: comments, a hash anywhere, leading spaces, carriage returns") { + auto text{definition::load("wagon").text()}; + // a commented line and one with a hash set the mass the wagon doesn't get; the rest, indented and ended with + // a carriage return, still load + auto const param{text.find("Param")}; + REQUIRE(param != std::string::npos); + text.insert(param, "// Param: M=11111\nParam: M=22222 # a note\n "); + std::string crlf; + for (auto const c : text) { + if (c == '\n') { + crlf += '\r'; + } + crlf += c; + } + TMoverParameters wagon(0.0, "wagon", "test", 0); + REQUIRE(wagon.LoadFIZ_Text(crlf)); + CHECK(wagon.Mass == doctest::Approx(42200.0)); + CHECK(wagon.Vmax == doctest::Approx(120.0)); + CHECK(wagon.MaxBrakePress[3] == doctest::Approx(3.8)); +} + +TEST_CASE("definition: a compressor list, whose positions allow the compressor and scale its speed and limits") { + // none of the game's vehicles has one. its rows are the positions after the 0th, which changes nothing: whether + // the compressor may run (2 yes, 1 no, 0 as the switch says), its speed, and its lower and upper limits. the + // table holds whole numbers (int CompressorList[4][9]), so a factor of 0.9 is read as 0, and both limits + // drop to nothing + auto eu07{definition::fiz("4e_v1/4e") + .set("CompressorList", "Size", 3) + .set("CompressorList", "Default", 1) + .set("CompressorList", "rows", {{2, 2, 1, 1}, {1, 1, 1, 1}, {2, 1, 0.9, 0.9}})}; + vehicle real{eu07, 0.0, "test", 1}; + CHECK(real->CompressorListPosNo == 3); + CHECK(real->CompressorList[3][3] == 0); + auto driven{test::driving(real)}; + test::track const line{0.15, 0.0, test::catenary_3kv}; + REQUIRE(test::start_up(driven, line)); + CHECK(real->CompressorListPos == 1); + test::drive(driven, line, 60.0); + CHECK(real->CompressorAllow); + CHECK(real->Compressor <= real->MaxCompressor + 0.05); + CHECK(real->Compressor >= real->MinCompressor - 0.05); + // the second position doesn't let it run + CHECK(real->ChangeCompressorPreset(2)); + test::drive(driven, line, 2.0); + CHECK_FALSE(real->CompressorAllow); + CHECK_FALSE(real->CompressorFlag); + // the third lets it, under limits of nothing: with no span between them for the governor, the reservoir over + // the upper limit gets vented while the compressor runs on + CHECK(real->ChangeCompressorPreset(3)); + auto const full{real->Compressor}; + test::drive(driven, line, 10.0); + CHECK(real->CompressorAllow); + CHECK(real->CompressorFlag); + CHECK(real->Compressor < full - 1.0); + // no further than the last position + CHECK_FALSE(real->ChangeCompressorPreset(5)); + CHECK(real->CompressorListPos == 3); +} + +TEST_CASE("definition: the compressor's limits set apart for the rear cab") { + auto eu07{definition::fiz("4e_v1/4e").set("Brake", "MinCP_B", 5.0).set("Brake", "MaxCP_B", 6.0)}; + for (auto const cab : {1, -1}) { + CAPTURE(cab); + vehicle real{eu07, 0.0, "test", cab}; + auto driven{test::driving(real)}; + test::track const line{0.15, 0.0, test::catenary_3kv}; + REQUIRE(test::start_up(driven, line)); + test::drive(driven, line, 120.0); + CHECK(real->CabDependentCompressor); + CHECK(real->MaxCompressor == doctest::Approx(cab > 0 ? 8.7 : 6.0)); + CHECK(real->MinCompressor == doctest::Approx(cab > 0 ? 7.0 : 5.0)); + CHECK(real->Compressor <= real->MaxCompressor + 0.05); + CHECK(real->Compressor >= real->MinCompressor - 0.05); + } +} + +TEST_CASE("definition: the highest power of an induction motor drive by the speed (PmaxList)") { + // the drive's power limit (eimc_p_Pmax) follows the table, between its rows and, by the last two, past its end + auto eu47{definition::fiz("e186_v2/eu47").set("PmaxList", "Size", 3).set("PmaxList", "rows", {{0, 1000}, {50, 3000}, {100, 5600}})}; + auto const table{[](double Speed) { return Speed < 50.0 ? 1000.0 + 40.0 * Speed : 3000.0 + 52.0 * (Speed - 50.0); }}; + for (auto const speed : {25.0, 75.0, 150.0}) { + CAPTURE(speed); + vehicle real{eu47, speed, "test", 1}; + auto driven{test::driving(real)}; + test::track const line{0.0, 0.0, test::catenary_3kv}; + REQUIRE(test::start_up(driven, line)); + CHECK(real->eimc[eimc_p_Pmax] == doctest::Approx(table(real->Vel)).epsilon(0.01)); + } +} + +TEST_CASE("definition: the colour and the brightness of the headlights") { + // only the 214M's own file has the section, which the definitions here can't hold (see data/fiz/README.md) + vehicle railbus{definition::fiz("21xm_v1/214ma") + .set("Headlights", "LampRed", 248) + .set("Headlights", "LampGreen", 221) + .set("Headlights", "LampBlue", 111) + .set("Headlights", "DimmedMultiplier", 0.4) + .set("Headlights", "NormalMultiplier", 0.9)}; + CHECK(railbus->refR == doctest::Approx(248.0)); + CHECK(railbus->refG == doctest::Approx(221.0)); + CHECK(railbus->refB == doctest::Approx(111.0)); + CHECK(railbus->dimMultiplier == doctest::Approx(0.4)); + CHECK(railbus->normMultiplier == doctest::Approx(0.9)); +} diff --git a/tests/unit/test_diesel.cpp b/tests/unit/test_diesel.cpp new file mode 100644 index 000000000..07686dc10 --- /dev/null +++ b/tests/unit/test_diesel.cpp @@ -0,0 +1,179 @@ +#include "stdafx.h" +#include "doctest.h" +#include "scenario.h" +#include "drive.h" + +#include "McZapkie/hamulce.h" + +// characterisation of the diesel engines besides their drive, recorded in golden traces (data/golden/diesel/): +// the heat of the engine, its oil and its water circuits, which keep a too cold or too hot engine from starting + +using test::scenario; +using namespace test::channels; + +namespace { + +std::vector heat() { + return { + {"Ts", [](auto const &M) { return M.dizel_heat.Ts; }}, + {"To", [](auto const &M) { return M.dizel_heat.To; }}, + {"Twy", [](auto const &M) { return M.dizel_heat.Twy; }}, + {"Twy2", [](auto const &M) { return M.dizel_heat.Twy2; }}, + {"PA", [](auto const &M) { return M.dizel_heat.PA ? 1.0 : 0.0; }}, + {"WaterHeater", [](auto const &M) { return M.WaterHeater.is_active ? 1.0 : 0.0; }}, + {"WaterPump", [](auto const &M) { return M.WaterPump.is_active ? 1.0 : 0.0; }}, + {"zaluzje1", [](auto const &M) { return M.dizel_heat.zaluzje1 ? 1.0 : 0.0; }}, + {"zaluzje2", [](auto const &M) { return M.dizel_heat.zaluzje2 ? 1.0 : 0.0; }}, + }; +} + +} // namespace + +TEST_CASE("diesel: an engine too cold to start, warmed by its heater") { + // at -10 degrees an engine with the lowest temperature of its water set (WaterMinTemperature) doesn't start, + // until the heater, which the driver switches on with the pumps, warms the water up; one without starts + struct engine { + char const *path; + bool starts; // cold + }; + for (auto const &[path, starts] : {engine{"su45_v2/301d", false}, engine{"sm42_v1/6d1", false}, engine{"st44_v2/m62-1151", true}}) { + CAPTURE(path); + scenario scenario{std::string{"diesel/cold/"} + path, traction() + heat(), test::track{}}; + scenario.add(path, 1); + auto &locomotive{scenario.cab()}; + locomotive.dizel_HeatSet(-10.f); + CHECK(scenario.start_up() == starts); + CHECK(locomotive.dizel_heat.PA != starts); + CHECK(locomotive.WaterHeater.is_active); + scenario.act("heating"); + scenario.wait(1200.0); + CHECK_FALSE(locomotive.dizel_heat.PA); + CHECK(locomotive.dizel_heat.Twy > 50.f); + CHECK(scenario.start_up()); + scenario.check(); + } +} + +TEST_CASE("diesel: an engine started hot, and run at full power") { + // at 88 degrees the engines with a second water circuit are over its highest temperature + // (WaterAuxMaxTemperature) and don't start while it cools; the SM42 has none, starts and gets cooled by its + // fans and shutters at full power. alone: a train's pipe, leaking under 2.75 bar while the engine + // doesn't start, gets cut off the FV4a handle of an LSt locomotive (lock_old in UpdatePipePressure) + struct engine { + char const *path; + bool starts; + }; + for (auto const &[path, starts] : {engine{"su45_v2/301d", false}, engine{"sm42_v1/6d1", true}, engine{"st44_v2/m62-1151", false}}) { + CAPTURE(path); + scenario scenario{std::string{"diesel/hot/"} + path, traction() + heat(), test::track{}}; + scenario.add(path, 1); + auto &locomotive{scenario.cab()}; + locomotive.dizel_HeatSet(88.f); + CHECK(scenario.start_up() == starts); + CHECK(locomotive.dizel_heat.PA != starts); + if (starts) { + scenario.get_going(); + scenario.full_power(300.0); + CHECK(locomotive.Vel > 50.0); + CHECK(locomotive.dizel_heat.zaluzje1); + CHECK(locomotive.dizel_heat.Twy < locomotive.dizel_heat.water.config.temp_max); + } + scenario.check(); + } +} + +TEST_CASE("diesel: the 214M railbus's torque converter, gears and retarder") { + // the controller of the railbus (EIMCtrlType 3) runs from the air brake (0, 1) through the retarder (2) and + // neutral (3) to holding (4) and raising (5) the power; the torque converter locks up at speed and the + // gearbox shifts by itself + scenario scenario{"diesel/214m", motion() + traction() + brakes(), test::track{}}; + scenario.add("21xm_v1/214ma", 1); + REQUIRE(scenario.start_up()); + auto &railbus{scenario.cab()}; + scenario.get_going(); + scenario.full_power(60.0); + CHECK(railbus.MainCtrlPos == 5); + CHECK(railbus.eimic_real == doctest::Approx(1.0)); + CHECK(railbus.hydro_TC_Lockup); + CHECK(railbus.ScndCtrlActualPos == railbus.ScndCtrlPosNo); + struct position { + double eimic; + bool retarder; + bool air; + }; + auto number{railbus.MainCtrlPos}; + for (auto const &[eimic, retarder, air] : {position{1.0, false, false}, position{0.0, false, false}, position{-1.0, true, false}, position{0.0, false, true}}) { + --number; + CAPTURE(number); + scenario.act(std::format("position {}", number)); + railbus.DecMainCtrl(1); + scenario.wait(8.0); + CHECK(railbus.eimic_real == doctest::Approx(eimic)); + CHECK((railbus.hydro_R_Fill > 0.99) == retarder); + CHECK((railbus.hydro_R_Torque > 1000.0) == retarder); + CHECK((railbus.BrakePress > 2.0) == air); + } + scenario.act("position 0"); + railbus.DecMainCtrl(1); + scenario.wait(30.0); + CHECK(railbus.Vel == doctest::Approx(0.0)); + // the gearbox shifted down as the railbus slowed + CHECK(railbus.ScndCtrlActualPos < railbus.ScndCtrlPosNo); + scenario.check(); +} + +TEST_CASE("diesel: the 214M railbus's speed control, up to the speed of a button and braking down to a lower one") { + // the second controller's first position takes the speed of the moment to hold; the buttons, the steps and + // the power limit change it. set lower than the speed, it brakes with the retarder (and under + // BrakeInterventionVel with the air); with the main controller off its top positions it stands by + scenario scenario{"diesel/214m speed control", motion() + traction() + brakes(), test::track{}}; + scenario.add("21xm_v1/214ma", 1); + REQUIRE(scenario.start_up()); + auto &railbus{scenario.cab()}; + REQUIRE(railbus.SpeedCtrl); + scenario.get_going(); + scenario.full_power(10.0); + scenario.act("speed control on"); + REQUIRE(railbus.IncScndCtrl(1)); + CHECK(railbus.SpeedCtrlUnit.IsActive); + CHECK(railbus.SpeedCtrlValue == doctest::Approx(std::round(railbus.Vel)).epsilon(0.02)); + scenario.act("button 80"); + railbus.SpeedCtrlButton(6); + CHECK(railbus.SpeedCtrlValue == doctest::Approx(80.0)); + scenario.wait(90.0); + CHECK(railbus.Vel == doctest::Approx(80.0).epsilon(0.02)); + CHECK_FALSE(railbus.SpeedCtrlUnit.Standby); + scenario.act("step down, up, power down and up"); + railbus.SpeedCtrlDec(); + CHECK(railbus.SpeedCtrlValue == doctest::Approx(75.0)); + railbus.SpeedCtrlInc(); + railbus.SpeedCtrlInc(); + CHECK(railbus.SpeedCtrlValue == doctest::Approx(85.0)); + CHECK(railbus.SpeedCtrlUnit.DesiredPower == doctest::Approx(1.0)); + CHECK(railbus.SpeedCtrlPowerDec()); + CHECK(railbus.SpeedCtrlUnit.DesiredPower == doctest::Approx(0.9)); + CHECK(railbus.SpeedCtrlPowerInc()); + CHECK(railbus.SpeedCtrlUnit.DesiredPower == doctest::Approx(1.0)); + scenario.wait(20.0); + scenario.act("button 40"); + railbus.SpeedCtrlButton(2); + scenario.wait(5.0); + // the retarder does it at this speed, the air isn't needed + CHECK(railbus.eimic_real == doctest::Approx(-1.0)); + CHECK(railbus.hydro_R_Fill == doctest::Approx(1.0)); + CHECK(railbus.BrakePress < 0.1); + CHECK_FALSE(railbus.SpeedCtrlUnit.BrakeInterventionBraking); + scenario.wait(60.0); + CHECK(railbus.Vel == doctest::Approx(40.0).epsilon(0.05)); + scenario.act("main controller back, standing by"); + railbus.DecMainCtrl(1); + railbus.DecMainCtrl(1); + railbus.DecMainCtrl(1); + scenario.wait(5.0); + CHECK(railbus.SpeedCtrlUnit.Standby); + scenario.act("speed control off"); + railbus.DecScndCtrl(2); + scenario.wait(5.0); + CHECK_FALSE(railbus.SpeedCtrlUnit.IsActive); + scenario.check(); +} diff --git a/tests/unit/test_figures.cpp b/tests/unit/test_figures.cpp new file mode 100644 index 000000000..c8b575d50 --- /dev/null +++ b/tests/unit/test_figures.cpp @@ -0,0 +1,67 @@ +#include "stdafx.h" +#include "doctest.h" + +#include + +#include "figures.h" +#include "vehicle.h" + +// the key figures of the golden traces (figures.h): the parts of a run between the events, and the comparison +// with the recorded ones, which lets the last decimal and half a percent go and tells the rest in readable lines + +using test::definition; +using test::vehicle; + +TEST_CASE("figures: a run split into parts by the events, each from the last step of the part before") { + vehicle eu07{definition::fiz("4e_v1/4e")}; + test::figures figures; + auto &mover{eu07.mover()}; + mover.V = 10.0; + mover.PipePress = 5.0; + figures.sample(0.0, mover); + figures.mark("brake"); + figures.mark("hard"); + for (int i = 1; i <= 10; ++i) { + mover.V = 10.0 - i; + mover.DistCounter += 0.005; + mover.PipePress = 3.5; + mover.BrakePress = 0.4 * i; + figures.sample(i, mover); + } + figures.mark("release"); + mover.PipePress = 4.0; + figures.sample(11.0, mover); + mover.PipePress = 5.0; + figures.sample(12.0, mover); + + auto const lines{figures.lines()}; + auto const has{[&lines](std::string const &Line) { return std::ranges::find(lines, Line) != lines.end(); }}; + CHECK(lines.front() == "[start]"); + // two events with nothing run between them make one part + CHECK(has("[brake, hard]")); + CHECK(has("duration = 10.00 s")); + CHECK(has("speed at the start = 36.0 km/h")); + CHECK(has("distance = 50.0 m")); + CHECK(has("stopped after = 10.00 s")); + CHECK(has("brake pipe lowest = 3.500 bar")); + CHECK(has("brake cylinders peak = 4.000 bar")); + CHECK(has("brake pipe back at 4.9 bar after = 2.00 s")); + // no current nor force anywhere in the run: left out of every part + CHECK_FALSE(has("motor current peak = 0 A")); +} + +TEST_CASE("figures: compared within half a percent or the last decimal, the differences told by part") { + std::vector const recorded{"[brake]", "distance = 412.3 m", "stopped after = 38.21 s", "brake cylinders peak = 3.800 bar"}; + CHECK(test::figures::compare(recorded, recorded).empty()); + CHECK(test::figures::compare(recorded, {"[brake]", "distance = 413.9 m", "stopped after = 38.22 s", "brake cylinders peak = 3.810 bar"}).empty()); + + auto const differences{test::figures::compare(recorded, {"[brake]", "distance = 398.0 m", "brake cylinders peak = 3.800 bar", "motor current peak = 10 A"})}; + REQUIRE(differences.size() == 3); + CHECK(differences[0] == "[brake] distance: 398.0 m now, 412.3 m recorded"); + CHECK(differences[1] == "[brake] stopped after: none now, 38.21 s recorded"); + CHECK(differences[2] == "[brake] motor current peak: 10 A now, none recorded"); + + auto const parts{test::figures::compare(recorded, {"[brake hard]", "distance = 412.3 m"})}; + REQUIRE(parts.size() == 1); + CHECK(parts[0] == "the parts differ from part 1: [brake hard] now, [brake] recorded"); +} diff --git a/tests/unit/test_fiz.cpp b/tests/unit/test_fiz.cpp new file mode 100644 index 000000000..f25632dee --- /dev/null +++ b/tests/unit/test_fiz.cpp @@ -0,0 +1,183 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" +#include "golden.h" +#include "real_vehicles.h" +#include "fiz.h" + +#include +#include +#include + +#include "utilities/parser.h" + +using test::definition; +using test::vehicle; + +// the real vehicle definitions in data/fiz load, as the game loads them, and give the parameters they gave +// when the traces got recorded: the loader (LoadFIZ_Parse and the parsing of every section) stays as it was + +TEST_CASE("fiz: every real vehicle loads and keeps its parameters") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle real{definition::fiz(path)}; + test::trace trace{"fiz/" + path, test::channels::parameters(), 1}; + trace.sample(0.0, real.mover()); + // what every vehicle has, the rest of its parameters are in the golden trace + CHECK(real->Mass > 0.0); + CHECK(real->TotalMass >= real->Mass); + CHECK(real->Vmax > 0.0); + CHECK(real->NAxles > 0); + CHECK(real->WheelDiameter > 0.0); + CHECK(real->NPoweredAxles <= real->NAxles); + trace.check(); + } +} + +TEST_CASE("fiz: changes made by a test reach the vehicle") { + auto const eu07{definition::fiz("4e_v1/4e")}; + CHECK(vehicle{eu07}->Mass == doctest::Approx(80000.0)); + CHECK(vehicle{definition{eu07}.set("Param", "M", 90000)}->Mass == doctest::Approx(90000.0)); + // a key the file doesn't have is added to its section + CHECK(vehicle{definition{eu07}.set("Brake", "MaxBP", 3.5)}->MaxBrakePress[3] == doctest::Approx(3.5)); + // removing a key brings its default back + CHECK(vehicle{definition{eu07}.remove("Param", "Vmax")}->Vmax != doctest::Approx(125.0)); +} + +namespace { + +// a section of the game's definition, as data/fiz keeps it +struct converted_section { + std::string name; + nlohmann::ordered_json keys = nlohmann::ordered_json::object(); + nlohmann::ordered_json rows = nlohmann::ordered_json::array(); +}; + +// the sections of the lines of the game's definition: the values of each section line, and the rows of a +// list until the line closing it. lines the loader skips, and the lines closing the lists, are left out +converted_section convert_section(test::fiz::section const &Section, std::string const &Line) { + converted_section converted{std::string{Section.name}}; + for (auto const &key : test::fiz::keys(Line)) { + if (auto const value{test::fiz::value(key, Line)}; false == value.empty()) { + converted.keys[key] = value; + } + } + return converted; +} + +nlohmann::ordered_json convert_row(std::string const &Line) { + auto row = nlohmann::ordered_json::array(); + for (auto const &word : test::fiz::words(Line)) { + row.push_back(word); + } + return row; +} + +std::vector convert(std::vector const &Lines) { + std::vector result; + auto reading_rows{false}; + for (auto const &line : Lines) { + if (auto const section{test::fiz::opened_by(line)}; section != nullptr) { + result.push_back(convert_section(*section, line)); + reading_rows = section->list != 0; + continue; + } + auto const list{result.empty() ? nullptr : test::fiz::named(result.back().name)}; + if (list == nullptr || list->end == nullptr) { + continue; + } + // an empty line closes the brake table of the Cntrl section, the other lists have lines of their own + if (std::string_view const end{list->end}; line.empty() ? end.empty() : (false == end.empty() && line.starts_with(end))) { + reading_rows = false; + continue; + } + if (reading_rows && false == line.empty()) { + result.back().rows.push_back(convert_row(line)); + } + } + return result; +} + +// the JSON of data/fiz: a section a line, a row of a list a line +std::string json_text(std::string const &Source, std::vector const &Sections) { + std::string text{"{\n \"_source\": " + nlohmann::ordered_json(Source).dump() + ",\n"}; + text += R"( "_comment": "converted by test_fiz.cpp from the game's definition with the files it includes, keeping what the game's loader reads")"; + for (auto const §ion : Sections) { + text += ",\n " + nlohmann::ordered_json(section.name).dump() + ": {"; + auto first{true}; + for (auto const &[key, value] : section.keys.items()) { + text += (first ? "" : ", ") + nlohmann::ordered_json(key).dump() + ": " + value.dump(); + first = false; + } + if (test::fiz::named(section.name)->list != 0) { + text += std::string{first ? "" : ", "} + "\"rows\": ["; + for (std::size_t row = 0; row < section.rows.size(); ++row) { + text += (row == 0 ? "\n " : ",\n ") + section.rows[row].dump(); + } + text += section.rows.empty() ? "]" : "\n ]"; + } + text += "}"; + } + return text + "\n}\n"; +} + +std::vector split(std::string_view const List) { + std::vector result; + std::size_t start{0}; + while (start < List.size()) { + auto const end{std::min(List.find(',', start), List.size())}; + if (end > start) { + result.emplace_back(List.substr(start, end - start)); + } + start = end + 1; + } + return result; +} + +} // namespace + +// converts the game's definitions of the real vehicles to data/fiz, from the folder of the game's vehicles given +// in EU07_UNIT_TESTS_FIZ_SOURCE (dynamic/): the vehicles there already, and the ones named in EU07_UNIT_TESTS_FIZ_ADD +// (paths without pkp/ and the extension, separated by commas). the game's parser reads the file with its includes, +// the conversion keeps what the loader reads of it, and checks the loader reads the same of the converted one +TEST_CASE("fiz: conversion of the game's definitions, when asked for") { + auto const *source{std::getenv("EU07_UNIT_TESTS_FIZ_SOURCE")}; + if (source == nullptr) { + return; + } + auto paths{test::real_vehicles()}; + if (auto const *added{std::getenv("EU07_UNIT_TESTS_FIZ_ADD")}; added != nullptr) { + for (auto const &path : split(added)) { + paths.push_back(path); + } + } + auto const root{std::filesystem::path{source}}; + for (auto const &path : paths) { + CAPTURE(path); + // the vehicles of the Polish railways are in pkp/, the others under their operators + auto file{root / (path + ".fiz")}; + if (false == std::filesystem::exists(file)) { + file = root / "pkp" / (path + ".fiz"); + } + REQUIRE(std::filesystem::exists(file)); + cParser parser(file.filename().string(), cParser::buffer_FILE, file.parent_path().generic_string() + "/"); + auto const original{test::fiz::lines(parser)}; + + auto const target{std::filesystem::path{EU07_UNIT_TESTS_DATA} / "fiz" / (path + ".json")}; + std::filesystem::create_directories(target.parent_path()); + { + std::ofstream output(target, std::ios::binary); + output << json_text(std::filesystem::relative(file, root).generic_string(), convert(original)); + } + cParser converted(definition::fiz(path).text(), cParser::buffer_TEXT); + auto const expected{test::fiz::reading(original)}; + auto const actual{test::fiz::reading(test::fiz::lines(converted))}; + auto const mismatch{std::ranges::mismatch(expected, actual)}; + if (mismatch.in1 != expected.end() || mismatch.in2 != actual.end()) { + std::filesystem::remove(target); + FAIL_CHECK("the loader reads the converted definition differently, at " << (mismatch.in1 != expected.end() ? *mismatch.in1 : "the end") << " against " + << (mismatch.in2 != actual.end() ? *mismatch.in2 : "the end")); + } + } +} diff --git a/tests/unit/test_invariants.cpp b/tests/unit/test_invariants.cpp new file mode 100644 index 000000000..36c511863 --- /dev/null +++ b/tests/unit/test_invariants.cpp @@ -0,0 +1,34 @@ +#include "stdafx.h" +#include "doctest.h" +#include "golden.h" +#include "vehicle.h" + +// the invariants every golden trace checks (invariants.h) catch a vehicle in a state the simulation must +// never reach, once each, naming the time and the last event + +using test::definition; +using test::vehicle; + +TEST_CASE("invariants: a vehicle in an impossible state, reported once with its time and the last event") { + vehicle eu07{definition::fiz("4e_v1/4e")}; + test::trace trace{"invariants/self test", test::channels::brakes()}; + trace.sample(0.0, eu07.mover()); + CHECK(trace.violations().empty()); + + trace.mark("overfilled"); + eu07->BrakePress = 50.0; + trace.sample(0.01, eu07.mover()); + trace.sample(0.02, eu07.mover()); + REQUIRE(trace.violations().size() == 1); + auto const &violation{trace.violations().front()}; + CHECK(violation.find("brake cylinders") != std::string::npos); + CHECK(violation.find("at 0.010 s") != std::string::npos); + CHECK(violation.find("after \"overfilled\"") != std::string::npos); + CHECK(violation.find("BrakePress=50") != std::string::npos); + + eu07->BrakePress = 0.0; + eu07->V = std::nan(""); + trace.sample(0.03, eu07.mover()); + CHECK(trace.violations().size() > 1); + CHECK(trace.violations()[1].find("every value a number") != std::string::npos); +} diff --git a/tests/unit/test_loads.cpp b/tests/unit/test_loads.cpp new file mode 100644 index 000000000..b4a9da62a --- /dev/null +++ b/tests/unit/test_loads.cpp @@ -0,0 +1,116 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// characterisation of the loads of vehicles besides the loading by commands (test_commands.cpp): the load +// assigned to a vehicle (AssignLoad), with the pantographs of an electric vehicle set up by the "pantstate" +// load of a scenario, and the steps of loading and unloading (LoadingDone) + +using test::definition; +using test::vehicle; + +TEST_CASE("loads: the pantographs of an electric vehicle set up by the amount of its pantstate load") { + // bit 0 raises the front pantograph, bit 1 the rear one, bit 2 turns the vehicle round in the train first + // (DoubleTr), so the bits name the pantographs the other way round + struct setup { + int amount; + bool frontraised; + bool rearraised; + }; + for (auto const &[amount, frontraised, rearraised] : {setup{0, false, false}, setup{1, true, false}, setup{2, false, true}, setup{3, true, true}, setup{5, false, true}, + setup{6, true, false}}) { + CAPTURE(amount); + vehicle et42{definition::fiz("et42_v2/112e-a")}; + CHECK(et42->AssignLoad("pantstate", static_cast(amount))); + CHECK(et42->DoubleTr == ((amount & 4) != 0 ? -1 : 1)); + CHECK(et42->Pantographs[end::front].valve.is_enabled == frontraised); + CHECK(et42->Pantographs[end::rear].valve.is_enabled == rearraised); + // the load itself stays as it was + CHECK(et42->LoadAmount == doctest::Approx(0.0)); + } + // a vehicle without a current collector takes no such load + vehicle wagon{definition::fiz("3wl_v2/3wl")}; + CHECK_FALSE(wagon->AssignLoad("pantstate", 3.f)); +} + +TEST_CASE("loads: a load assigned, of one kind at a time, up to the vehicle's capacity, and emptied") { + vehicle wagon{definition::fiz("3wl_v2/3wl")}; + auto const empty{wagon->TotalMass}; + REQUIRE(wagon->AssignLoad("coal", 10.f)); + CHECK(wagon->LoadTypeChange); + CHECK(wagon->LoadType.name == "coal"); + CHECK(wagon->TotalMass > empty); + // more of the same: the amount set, not added, and no more than the vehicle takes + wagon->LoadTypeChange = false; + REQUIRE(wagon->AssignLoad("coal", 1000.f)); + CHECK_FALSE(wagon->LoadTypeChange); + CHECK(wagon->LoadAmount == doctest::Approx(wagon->MaxLoad)); + // another kind can't be added to a load, nor can an unknown one be loaded + CHECK_FALSE(wagon->AssignLoad("ruda", 5.f)); + CHECK(wagon->LoadType.name == "coal"); + // emptied, with no name + REQUIRE(wagon->AssignLoad("")); + CHECK(wagon->LoadTypeChange); + CHECK(wagon->LoadAmount == doctest::Approx(0.0)); + CHECK(wagon->LoadType.name.empty()); + CHECK_FALSE(wagon->AssignLoad("unknown", 5.f)); + CHECK(wagon->AssignLoad("ruda", 5.f)); +} + +TEST_CASE("loads: the steps of loading and unloading") { + // most of the game's wagons may be overloaded (OverLoadFactor), the 3WL not + vehicle wagon{definition::fiz("3wl_v2/3wl").set("Load", "OverLoadFactor", 1.5)}; + REQUIRE(wagon->AssignLoad("coal", 10.f)); + auto const capacity{wagon->MaxLoad * (1.0 + wagon->OverLoadFactor)}; + + SUBCASE("no speed of the change: done at once") { + CHECK(wagon->LoadingDone(0.0, "coal")); + CHECK(wagon->LoadStatus == 4); + } + SUBCASE("no load, or another one, named: done when it was") { + wagon->LoadStatus = 1; + CHECK_FALSE(wagon->LoadingDone(1.0, "")); + CHECK_FALSE(wagon->LoadingDone(1.0, "ruda")); + wagon->LoadStatus = 4; + CHECK(wagon->LoadingDone(1.0, "")); + } + SUBCASE("unloading, step by step, what was asked or all of it, the name going with the last of it") { + wagon->CommandIn.Value1 = 4.0; // asked to unload + wagon->LastLoadChangeTime = 2.0; // at 1 a second + CHECK_FALSE(wagon->LoadingDone(-1.0, "coal")); + CHECK(wagon->LoadStatus == 2); + CHECK(wagon->LoadAmount == doctest::Approx(8.0)); + CHECK(wagon->LastLoadChangeTime == doctest::Approx(0.0)); + // nothing done since the last step + CHECK_FALSE(wagon->LoadingDone(-1.0, "coal")); + CHECK(wagon->LoadAmount == doctest::Approx(8.0)); + wagon->LastLoadChangeTime = 2.0; + CHECK(wagon->LoadingDone(-1.0, "coal")); + CHECK(wagon->LoadAmount == doctest::Approx(6.0)); + wagon->CommandIn.Value1 = 100.0; + wagon->LastLoadChangeTime = 10.0; + CHECK(wagon->LoadingDone(-1.0, "coal")); + CHECK(wagon->LoadAmount == doctest::Approx(0.0)); + CHECK(wagon->LoadType.name.empty()); + } + SUBCASE("loading, step by step, up to what was asked or what the vehicle takes, slower when overloaded") { + wagon->CommandIn.Value1 = 5.0; + wagon->LastLoadChangeTime = 2.0; + CHECK_FALSE(wagon->LoadingDone(1.0, "coal")); + CHECK(wagon->LoadStatus == 1); + CHECK(wagon->LoadAmount == doctest::Approx(12.0)); + wagon->LastLoadChangeTime = 3.0; + CHECK(wagon->LoadingDone(1.0, "coal")); + CHECK(wagon->LoadAmount == doctest::Approx(15.0)); + wagon->CommandIn.Value1 = 1000.0; + wagon->LastLoadChangeTime = 1000.0; + CHECK(wagon->LoadingDone(1.0, "coal")); + CHECK(wagon->LoadAmount == doctest::Approx(capacity)); + CHECK(capacity == doctest::Approx(2.5 * wagon->MaxLoad)); + // over the vehicle's own capacity, the change goes at half the speed + wagon->CommandIn.Value1 = 1000.0; + wagon->LastLoadChangeTime = 4.0; + CHECK_FALSE(wagon->LoadingDone(-1.0, "coal")); + CHECK(wagon->LoadAmount == doctest::Approx(capacity - 2.0)); + } +} diff --git a/tests/unit/test_queries.cpp b/tests/unit/test_queries.cpp new file mode 100644 index 000000000..d42232c2e --- /dev/null +++ b/tests/unit/test_queries.cpp @@ -0,0 +1,61 @@ +#include "stdafx.h" +#include "doctest.h" +#include "vehicle.h" + +// characterisation of what the game's panels read of a vehicle besides the gauges recorded in the traces: the +// description of its damage on the driver's panel (driveruipanels.cpp) + +using test::definition; +using test::vehicle; + +TEST_CASE("queries: the damage of a vehicle as the driver's panel describes it") { + vehicle locomotive{definition::fiz("4e_v1/4e")}; + vehicle wagon{definition::fiz("3wl_v2/3wl")}; + // a powered locomotive and a wagon read some of the flags differently + locomotive->Power = 2000.0; + REQUIRE(wagon->Power < 0.1); + + struct damage { + int flag; + char const *locomotive; + char const *wagon; + }; + for (auto const &[flag, locomotivetext, wagontext] : { + damage{0, "OK", "OK"}, + damage{dtrain_thinwheel, "Thin wheel", "Load shifted"}, + damage{dtrain_wheelwear, "Wheel wear", "Wheel wear"}, + damage{dtrain_bearing, "Bearing damaged", "Bearing damaged"}, + damage{dtrain_coupling, "Coupler broken", "Coupler broken"}, + damage{dtrain_loaddamage, "Ventilator damaged", "Load damaged"}, + damage{dtrain_loaddestroyed, "Engine damaged", "LOAD DESTROYED"}, + damage{dtrain_axle, "Axle broken", "Axle broken"}, + damage{dtrain_out, "DERAILED", "DERAILED"}, + // the last flag of the list wins + damage{dtrain_bearing | dtrain_axle, "Axle broken", "Axle broken"}, + damage{255, "WRECKED", "WRECKED"}, + }) { + CAPTURE(flag); + locomotive->DamageFlag = flag; + wagon->DamageFlag = flag; + CHECK(locomotive->EngineDescription(0) == locomotivetext); + CHECK(wagon->EngineDescription(0) == wagontext); + } + // flat wheels count as worn ones + wagon->DamageFlag = 0; + wagon->WheelFlat = 6.0; + CHECK(wagon->EngineDescription(0) == "Wheel wear"); + // nothing else is described + CHECK(wagon->EngineDescription(1) == "Invalid qualifier"); +} + +TEST_CASE("queries: the ammeters of a vehicle without power show the current of the ones it controls") { + vehicle unpowered{definition::fiz("en57_v1/6ba_840")}; + vehicle powered{definition::fiz("en57_v1/5bgb")}; + unpowered->Attach(end::rear, end::front, &powered.mover(), coupling::control, true, false); + powered->Power = 600.0; + powered->Itot = 321.7; + unpowered->Power = 0.0; + CHECK(unpowered->ShowCurrent(0) == doctest::Approx(321.0)); // whole amperes + powered->Power = 0.0; + CHECK(unpowered->ShowCurrent(0) == doctest::Approx(0.0)); +} diff --git a/tests/unit/test_traction.cpp b/tests/unit/test_traction.cpp new file mode 100644 index 000000000..6da4ca506 --- /dev/null +++ b/tests/unit/test_traction.cpp @@ -0,0 +1,270 @@ +#include "stdafx.h" + +#include + +#include "doctest.h" +#include "vehicle.h" +#include "drive.h" +#include "golden.h" +#include "real_vehicles.h" +#include "scenario.h" + +#include "McZapkie/hamulce.h" + +using test::definition; +using test::vehicle; + +// characterisation of the drive of every real powered vehicle, driven alone from its front cab under a 3 kV +// catenary through test::drive_cycle(), in golden traces (data/golden/traction/). the cars of the electric +// units which take power or battery from the rest of their unit don't start alone: their traces record +// that, test_consist.cpp drives the whole units + +TEST_CASE("traction: every real powered vehicle started up, driven to full power and braked to a stop") { + for (auto const &path : test::real_vehicles()) { + CAPTURE(path); + test::reseed_random(); + vehicle real{definition::fiz(path), 0.0, "test", 1}; + if (false == test::is_powered(real.mover())) { + continue; + } + using namespace test::channels; + test::trace trace{"traction/" + path, motion() + traction() + air() + brakes(), 100}; + real.record(trace); + test::drive_cycle(test::driving(real), test::track{0.15, 0.0, test::catenary_3kv}, trace); + // every vehicle getting its line breaker closed runs off and is braked to a stop. the SM31 and the TEM2 + // barely move (COVERAGE.md); the cars of a unit taking their power from another car don't start alone + if (real->Mains) { + auto const crawls{path == "sm31_v1/411d" || path == "tem2_v2/tem2a"}; + CHECK((real->DistCounter > 0.2 /* km */) != crawls); + // but the road vehicle, which a minute of full service only slows down + CHECK((real->Vel < 0.5) != (path == "road/man/nl293")); + } + trace.check(); + } +} + +namespace { + +using test::scenario; +using namespace test::channels; + +void stop(scenario &Scenario) { + Scenario.notch_back(0); + Scenario.act("brake"); + Scenario.cab().BrakeLevelSet(Scenario.cab().Handle->GetPos(bh_FB)); + Scenario.wait(60.0); +} + +} // namespace + +TEST_CASE("traction: the line breaker opened by the catenary lost, and by too high a voltage") { + // the voltage relays of the series motors, and the induction motors' own limits: under MinV of the current + // collector, or over MaxV (with the overvoltage protection, OVP) and over MaxV + 200 V for the induction motors + for (auto const &[name, vehicles] : std::vector>>{ + {"eu07", {"4e_v1/4e"}}, {"et42", {"et42_v2/112e-a", "et42_v2/112e-b"}}, {"eu47", {"e186_v2/eu47"}}}) { + CAPTURE(name); + scenario scenario{"traction/catenary/" + name, motion() + traction()}; + for (auto const *path : vehicles) { + scenario.add(path, scenario.train().size() == 0 ? 1 : 0); + } + scenario.add("bdhpumn_v3/bdhpumn"); + REQUIRE(scenario.start_up()); + scenario.get_going(); + auto &controlling{scenario.driver().controlling}; + scenario.full_power(10.0); + REQUIRE(controlling.Mains); + scenario.act("catenary lost"); + scenario.line().voltage = 0.0; + scenario.wait(5.0); + CHECK_FALSE(controlling.Mains); + CHECK(controlling.Im == doctest::Approx(0.0)); + scenario.act("catenary back"); + scenario.line().voltage = test::catenary_3kv; + scenario.notch_back(0); + CHECK(scenario.start_up()); + scenario.full_power(5.0); + scenario.act("overvoltage"); + scenario.line().voltage = controlling.EnginePowerSource.CollectorParameters.MaxV + 300.0; + scenario.wait(5.0); + CHECK(controlling.Mains == (controlling.EngineType == TEngineType::ElectricSeriesMotor && false == controlling.EnginePowerSource.CollectorParameters.OVP)); + scenario.line().voltage = test::catenary_3kv; + stop(scenario); + scenario.check(); + } +} + +TEST_CASE("traction: the ET42 at the high current setting, its halves in series, and its switched electrodynamic brake") { + for (auto const high : {false, true}) { + CAPTURE(high); + scenario scenario{std::string{"traction/et42/"} + (high ? "high current" : "low current"), motion() + traction() + brakes()}; + scenario.add("et42_v2/112e-a", 1).add("et42_v2/112e-b").add("3wl_v2/3wl").add("3wl_v2/3wl").add("3wl_v2/3wl"); + REQUIRE(scenario.start_up()); + auto &et42{scenario.cab()}; + scenario.act("current setting"); + CHECK(et42.CurrentSwitch(high) == high); + // the relay takes the setting at the next step, with the controller at zero + scenario.get_going(); + CHECK((et42.Imax == et42.ImaxHi) == high); + scenario.full_power(60.0); + // at the high current setting the controller stops at the last series position: the halves' motors stay + // in series, at half the voltage each, for a heavy start, and the train crawls + CHECK((et42.RList[et42.MainCtrlPos].Bn > 1) != high); + CHECK(et42.Vel > (high ? 30.0 : 90.0)); + CHECK(et42.Vel < (high ? 50.0 : 110.0)); + scenario.act("controller to zero"); + while (et42.DecMainCtrl(2)) { + // down to zero + } + scenario.wait(2.0); + scenario.act("dynamic brake"); + CHECK(et42.DynamicBrakeSwitch(true)); + scenario.wait(5.0); + scenario.full_power(20.0); + // the controller sets the brake's strength. from the low setting's top speed, over 100 km/h, the motors' + // current goes over 300 A at the first position and the overload relay trips, leaving the train to roll; + // from the high setting's, under 40 km/h, the brake stops the train + CHECK(et42.FuseFlag != high); + CHECK((et42.Vel == doctest::Approx(0.0)) == high); + scenario.act("dynamic brake off"); + scenario.notch_back(0); + et42.DynamicBrakeSwitch(false); + stop(scenario); + scenario.check(); + } +} + +TEST_CASE("traction: the field shunting relays of the diesel-electric locomotives, at full power and notched back") { + // each kind of the automatic relays (RelayType) the game's diesel-electrics have, run with a wagon up to full + // shunting (and past their top speed, which nothing limits) and notched back through the positions where the + // relays let the shunting go: by the current (0), by the speed alone (1, which keeps it at any position), by + // the current at the full position only (41), by the position and the speed (45, 46) + struct locomotive { + char const *path; + int relaytype; + std::array shunting; // after notching back to 11, 10, 7 and 3 + }; + for (auto const &[path, relaytype, shunting] : {locomotive{"cd/754_v1/754", 0, {3, 3, 3, 0}}, locomotive{"sm31_v1/411d", 1, {2, 2, 2, 2}}, + locomotive{"sm40_v1/sm40", 41, {1, 1, 1, 1}}, locomotive{"su45_v2/301d", 45, {3, 2, 0, 0}}, + locomotive{"su46_v2/303d", 46, {4, 2, 0, 0}}}) { + CAPTURE(path); + scenario scenario{std::string{"traction/relays/"} + path, motion() + traction()}; + scenario.add(path, 1).add("bdhpumn_v3/bdhpumn"); + REQUIRE(scenario.start_up()); + auto &controlling{scenario.driver().controlling}; + CHECK(controlling.RelayType == relaytype); + scenario.get_going(); + scenario.full_power(120.0); + CHECK(controlling.ScndCtrlPos == controlling.ScndCtrlPosNo); + auto expected{shunting.begin()}; + for (auto const position : {11, 10, 7, 3}) { + CAPTURE(position); + scenario.act(std::format("notch back to {}", position)); + scenario.notch_back(position); + scenario.wait(15.0); + CHECK(controlling.ScndCtrlPos == *expected++); + } + stop(scenario); + scenario.check(); + } +} + +TEST_CASE("traction: the stages of the EW58's electrodynamic brake, set by the positions of its ep handle") { + // the FVE408's positions 1 to 5 give the ep brake's strength, whose whole part is the stage of the motors' + // brake (TUHEX): 1, 1, 2, 3, 3; over 100 km/h the motors brake at the first stage alone + scenario scenario{"traction/ew58 brake stages", motion() + traction() + brakes()}; + for (auto const *car : {"ew58_v1/ew58-sa", "ew58_v1/ew58-d", "ew58_v1/ew58-sb"}) { + scenario.add(car, scenario.train().size() == 0 ? 1 : 0, test::consist::full_coupling, 115.0); + } + REQUIRE(scenario.start_up()); + auto &ew58{scenario.cab()}; + for (auto const &[position, stage] : {std::pair{1, 1}, std::pair{3, 2}, std::pair{4, 3}, std::pair{2, 1}, std::pair{5, 3}}) { + CAPTURE(position); + scenario.act(std::format("position {}", position)); + ew58.BrakeLevelSet(position); + scenario.wait(6.0); + CHECK(ew58.TUHEX_StageActual == stage); + CHECK(ew58.DynamicBrakeFlag); + CHECK(ew58.Im < 0.0); + } + scenario.act("release"); + ew58.BrakeLevelSet(ew58.Handle->GetPos(bh_RP)); + scenario.wait(10.0); + CHECK(ew58.TUHEX_StageActual == 0); + scenario.check(); +} + +TEST_CASE("traction: the induction motor drive's speed control, its electrodynamic brake by the local brake, and at the voltage's limit") { + scenario scenario{"traction/eu47 drive", motion() + traction() + brakes()}; + scenario.add("e186_v2/eu47", 1).add("bdhpumn_v3/bdhpumn").add("bdhpumn_v3/bdhpumn"); + REQUIRE(scenario.start_up()); + auto &eu47{scenario.cab()}; + scenario.get_going(); + // the second controller of the EU47 sets the speed to hold (SpeedCtrlType=Time): position 2 keeps the speed + // set, 3 raises it by 10 km/h every SpeedCtrlDelay, 4 sets the top speed, 1 lowers it, 0 drops it + REQUIRE(eu47.ScndCtrlPosNo == 4); + REQUIRE(eu47.SpeedCtrlTypeTime); + scenario.full_power(0.0); + scenario.act("speed up"); + for (int i = 0; i < 3; ++i) { + eu47.IncScndCtrl(1); + } + scenario.wait(3.0); + scenario.act("hold"); + eu47.DecScndCtrl(1); + scenario.wait(1.0); + CHECK(eu47.SpeedCtrlValue == doctest::Approx(60.0)); + scenario.wait(60.0); + CHECK(eu47.Vel == doctest::Approx(60.0).epsilon(0.01)); + scenario.act("slow down"); + eu47.DecScndCtrl(1); + scenario.wait(1.5); + eu47.IncScndCtrl(1); + scenario.wait(1.0); + CHECK(eu47.SpeedCtrlValue == doctest::Approx(20.0)); + // a lower speed set only takes the power off, the train rolls on + scenario.wait(40.0); + CHECK(eu47.Vel > 50.0); + scenario.act("top speed"); + eu47.IncScndCtrl(2); + scenario.wait(5.0); + CHECK(eu47.SpeedCtrlValue == doctest::Approx(eu47.Vmax)); + scenario.act("speed control off"); + while (eu47.DecScndCtrl(1)) { + // down to zero + } + scenario.notch_back(0); + scenario.wait(2.0); + CHECK(eu47.SpeedCtrlValue == doctest::Approx(0.0)); + // the local brake sets the flag of the electrodynamic brake, which keeps the train brake's air out of the + // cylinders, but the motors' brake follows the train brake alone: the local brake's air does the braking + scenario.act("local brake"); + eu47.IncLocalBrakeLevel(LocalBrakePosNo); + scenario.wait(5.0); + CHECK(eu47.DynamicBrakeFlag); + CHECK(eu47.Im == doctest::Approx(0.0)); + CHECK(eu47.BrakePress > 2.0); + scenario.act("local brake off"); + eu47.DecLocalBrakeLevel(LocalBrakePosNo); + scenario.wait(10.0); + // braking back into the catenary: at its usual voltage, and at its highest, over the drive's MaxV, where + // the drive lets less of the current go back to it (Vadd) + for (auto const voltage : {test::catenary_3kv, eu47.EnginePowerSource.CollectorParameters.MaxV + 100.0}) { + CAPTURE(voltage); + scenario.line().voltage = voltage; + scenario.act(std::format("service brake at {} V", static_cast(voltage))); + eu47.BrakeLevelSet(eu47.Handle->GetPos(bh_FB)); + scenario.wait(6.0); + CHECK(eu47.DynamicBrakeFlag); + CHECK(eu47.Im < -100.0); + CHECK((eu47.Vadd > 1.0) == (voltage > eu47.EnginePowerSource.CollectorParameters.MaxV)); + eu47.BrakeLevelSet(eu47.Handle->GetPos(bh_RP)); + scenario.wait(6.0); + } + scenario.line().voltage = test::catenary_3kv; + scenario.act("line breaker off"); + eu47.MainSwitch(false); + scenario.wait(5.0); + CHECK(eu47.Im == doctest::Approx(0.0)); + stop(scenario); + scenario.check(); +} diff --git a/tests/unit/vehicle.cpp b/tests/unit/vehicle.cpp index 914a31079..e38999f7d 100644 --- a/tests/unit/vehicle.cpp +++ b/tests/unit/vehicle.cpp @@ -1,60 +1,26 @@ #include "stdafx.h" #include "vehicle.h" +#include "golden.h" +#include "real_vehicles.h" +#include "fiz.h" #include "vehicle/DynObj.h" +#include #include #include +#include "utilities/Globals.h" + namespace test { namespace { -struct section_format { - char separator; // the game's definitions end section names with either '.' or ':' - char const *table_end{nullptr}; // the line closing the rows of a table section -}; - -// the sections TMoverParameters::LoadFIZ() reads, see its issection() calls. a section missing here -// would be silently skipped by the loader, and the test would run on the defaults instead -std::map> const sections{ - {"Param", {'.'}}, - {"Load", {':'}}, - {"Dimensions", {':'}}, - {"Wheels", {':'}}, - {"Brake", {':'}}, - {"Doors", {':'}}, - {"BuffCoupl", {'.'}}, - {"BuffCoupl1", {'.'}}, - {"BuffCoupl2", {'.'}}, - {"TurboPos", {':'}}, - {"Cntrl", {'.'}}, - {"Blending", {':'}}, - {"DCEMUED", {':'}}, - {"SpringBrake", {':'}}, - {"Light", {':'}}, - {"Headlights", {':'}}, - {"Clima", {':'}}, - {"Power", {':'}}, - {"SpeedControl", {':'}}, - {"Engine", {':'}}, - {"Switches", {':'}}, - {"Security", {':'}}, - {"Circuit", {':'}}, - {"AI", {':'}}, - {"LightsList", {':', "endL"}}, - {"DimmerList", {':', "endDimmerList"}}, - {"WiperList", {':', "endwl"}}, - {"CompressorList", {':', "endCL"}}, - {"RList", {':', "END-RL"}}, - {"MotorParamTable0", {':', "END-MPT"}}, -}; - -section_format const &format_of(std::string const &Section) { - auto const lookup{sections.find(Section)}; - if (lookup == sections.end()) { - throw definition_error("unknown vehicle definition section " + Section + ", see tests/unit/vehicle.cpp"); +fiz::section const &format_of(std::string const &Section) { + auto const section{fiz::named(Section)}; + if (section == nullptr) { + throw definition_error("vehicle definition section " + Section + " isn't one the loader reads, see tests/unit/fiz.cpp"); } - return lookup->second; + return *section; } std::string value_text(nlohmann::ordered_json const &Value) { @@ -67,9 +33,9 @@ std::string value_text(nlohmann::ordered_json const &Value) { return Value.dump(); } -// the rows of a table section, each a list of values, and the line closing them -std::string table_text(std::string const &Section, section_format const &Format, nlohmann::ordered_json const &Rows) { - if (Format.table_end == nullptr) { +// the rows of a list section, each a list of values, and the line closing them +std::string table_text(std::string const &Section, fiz::section const &Format, nlohmann::ordered_json const &Rows) { + if (Format.end == nullptr) { throw definition_error("vehicle definition section " + Section + " has no rows"); } std::string text; @@ -79,34 +45,47 @@ std::string table_text(std::string const &Section, section_format const &Format, } text += "\n"; } - return text + Format.table_end + "\n"; + return text + Format.end + "\n"; } -} // namespace - -definition definition::load(std::string const &Name) { - auto const path{std::string{EU07_UNIT_TESTS_DATA} + "/" + Name + ".json"}; - std::ifstream file(path); +nlohmann::ordered_json sections_of(std::string const &Path) { + std::ifstream file(Path); if (false == file.is_open()) { - throw definition_error("no test vehicle definition " + path); + throw definition_error("no test vehicle definition " + Path); } - definition result; // not brace-initialised: json{other} makes an array of other with GCC and Clang (the initializer list constructor wins) auto const json = nlohmann::ordered_json::parse(file, nullptr, true, true); if (false == json.is_object()) { - throw definition_error("test vehicle definition " + path + " isn't a JSON object"); + throw definition_error("test vehicle definition " + Path + " isn't a JSON object"); } + auto result = nlohmann::ordered_json::object(); for (auto const &[section, keys] : json.items()) { // keys starting with _ are comments if (section.starts_with('_')) { continue; } format_of(section); - result.m_sections[section] = keys; + result[section] = keys; } return result; } +} // namespace + +definition definition::load(std::string const &Name) { + definition result; + result.m_sections = sections_of(std::string{EU07_UNIT_TESTS_DATA} + "/" + Name + ".json"); + return result; +} + +definition definition::fiz(std::string const &Path) { + definition result; + result.m_sections = sections_of(std::string{EU07_UNIT_TESTS_DATA} + "/fiz/" + Path + ".json"); + auto const slash{Path.rfind('/')}; + result.m_typename = slash == std::string::npos ? Path : Path.substr(slash + 1); + return result; +} + definition &definition::set(std::string const &Section, std::string const &Key, nlohmann::ordered_json const &Value) { format_of(Section); m_sections[Section][Key] = Value; @@ -143,52 +122,145 @@ std::string definition::text() const { return text; } -vehicle::vehicle(definition const &Definition, double Velocity, std::string const &Name) { - // the same steps as TDynamicObject::Init() - m_mover = std::make_unique(Velocity, Name, Name, 0); +vehicle::vehicle(definition const &Definition, double Velocity, std::string const &Name, int Cab) : m_cab(Cab) { + // the same steps as TDynamicObject::Init(), for a vehicle set the usual way round (iDirection 1) + auto const typename_{Definition.type_name().empty() ? Name : Definition.type_name()}; + m_mover = std::make_unique(Velocity, typename_, Name, Cab); if (false == m_mover->LoadFIZ_Text(Definition.text())) { throw definition_error("the test vehicle definition didn't load"); } m_mover->AssignLoad("", 0.f); m_mover->ComputeMass(); auto const driveractive{Velocity != 0.0}; - if (false == m_mover->CheckLocomotiveParameters(driveractive, Velocity >= 0.0 ? 1 : -1)) { + if (false == m_mover->CheckLocomotiveParameters(driveractive, (Velocity > 0 ? 1 : -1) * Cab)) { throw definition_error("parameters mismatch in the test vehicle definition"); } m_mover->MainCtrlPos = m_mover->MainCtrlNoPowerPos(); m_mover->LocalBrakePosA = 0.0; - m_mover->BrakeCtrlPos = static_cast(std::floor(m_mover->Handle->GetPos(driveractive ? bh_RP : bh_NP))); + if (driveractive) { + m_mover->BrakeCtrlPos = static_cast(std::floor(m_mover->Handle->GetPos(Cab == 0 ? bh_NP : bh_RP))); + // a driver keeps a vehicle on standby on its independent brake + if (Cab != 0 && m_mover->LocalBrake != TLocalBrake::ManualBrake && Velocity < 1.0) { + m_mover->IncLocalBrakeLevel(LocalBrakePosNo); + if (m_mover->EIMCtrlEmergency) { + m_mover->DecLocalBrakeLevel(1); + } + } + } + else { + m_mover->BrakeCtrlPos = static_cast(std::floor(m_mover->Handle->GetPos(bh_NP))); + } m_mover->BrakeLevelSet(m_mover->BrakeCtrlPos); } void vehicle::step(track const &Track, double Dt) { - compute_forces(Dt); - compute_movement(Track, Dt, TLocation{0.0, 0.0, 0.0}); -} - -void vehicle::compute_forces(double Dt) { - if (m_pipepressure >= 0.0) { - m_mover->Pipe->CreatePress(m_pipepressure); + begin_step(); + TLocation const location{0.0, 0.0, 0.0}; + for (int iteration = 1; iteration < Track.iterations; ++iteration) { + forces(Dt); + fast_move(Track, Dt, location); } - m_mover->ComputeTotalForce(Dt); + forces(Dt); + move(Track, Dt, location, m_mover->PipePress); } -double vehicle::compute_movement(track const &Track, double Dt, TLocation const &Location) { +namespace { + +TTrackShape shape_of(track const &Track) { TTrackShape shape; shape.Len = 1000.0; shape.dHtrack = Track.gradient; + return shape; +} + +TTrackParam parameters_of(track const &Track, TMoverParameters const &Mover) { TTrackParam param; - param.Width = 1.435; + param.Width = Track.width; param.friction = Track.friction; - param.CategoryFlag = 1; - TTractionParam const traction; + // the vehicle's own kind of way: a road vehicle on rails derails as soon as it moves + if (Track.category != 0) { + param.CategoryFlag = Track.category; + } + else { + param.CategoryFlag = Mover.CategoryFlag != 0 ? Mover.CategoryFlag : 1; + } + return param; +} + +} // namespace + +double vehicle::fast_move(track const &Track, double Dt, TLocation const &Location) { + // TDynamicObject::FastUpdate(), the iterations of a frame before the last one + auto param{parameters_of(Track, *m_mover)}; + return m_mover->FastComputeMovement(Dt, shape_of(Track), param, Location, TRotation{0.0, 0.0, 0.0}); +} + +void vehicle::begin_step() { + // the conditions the test keeps + if (m_pipepressure >= 0.0) { + m_mover->Pipe->CreatePress(m_pipepressure); + } + if (m_reservoirpressure >= 0.0 && m_mover->VeselVolume > 0.0) { + m_mover->CompressedVolume = m_reservoirpressure * m_mover->VeselVolume; + } + // vehicle_table::update(), before the forces + m_mover->ComputeConstans(); +} + +void vehicle::forces(double Dt) { + // TDynamicObject::UpdateForce() + m_mover->ComputeTotalForce(Dt); +} + +double vehicle::move(track const &Track, double Dt, TLocation const &Location, double Trainpipe) { + auto const shape{shape_of(Track)}; + auto const param{parameters_of(Track, *m_mover)}; TRotation const rotation{0.0, 0.0, 0.0}; - return m_mover->ComputeMovement(Dt, Dt, shape, param, traction, Location, rotation); + + // what TDynamicObject::Update() does before the movement. a raised pantograph touches the wire at once, + // the game animates its rise + auto &mover{*m_mover}; + mover.PantFrontVolt = mover.Pantographs[end::front].is_active ? Track.voltage : 0.0; + mover.PantRearVolt = mover.Pantographs[end::rear].is_active ? Track.voltage : 0.0; + TTractionParam traction; + traction.TractionVoltage = + mover.EnginePowerSource.SourceType == TPowerSource::CurrentCollector ? std::max(std::abs(mover.PantRearVolt), std::abs(mover.PantFrontVolt)) : 0.95 * mover.EnginePowerSource.MaxVoltage; + traction.TractionFreq = 0; + traction.TractionMaxCurrent = 7500; + traction.TractionResistivity = 0.3; + mover.PantographVoltage = traction.TractionVoltage; + if (m_cab != 0) { + using enum TEngineType; + // the driver's own vehicle, which gets the average pressure of the train's brake pipe + mover.EqvtPipePress = Trainpipe; + if (auto const diesel{mover.EngineType == DieselEngine || mover.EngineType == DieselElectric}; diesel && mover.EIMCtrlType > 0) { + mover.CheckEIMIC(Dt); + if (mover.SpeedCtrl) { + mover.CheckSpeedCtrl(Dt); + } + mover.eimic_real = std::min(mover.eimic, mover.eimicSpeedCtrl); + } + if (mover.EngineType == ElectricInductionMotor) { + // the share of the electric and pneumatic brake over the train (TDynamicObject's MED) isn't here + mover.CheckEIMIC(Dt); + mover.CheckSpeedCtrl(Dt); + mover.eimic_real = std::min(mover.eimic, mover.eimicSpeedCtrl); + } + } + + // the whole frame's time, of all its iterations + auto const frame{Dt * Track.iterations}; + auto const distance{m_mover->ComputeMovement(Dt, frame, shape, param, traction, Location, rotation)}; + m_time += frame; + if (m_trace != nullptr) { + m_trace->sample(m_time, *m_mover); + } + return distance; } void vehicle::run(double Seconds, track const &Track, double Dt) { - // whole steps: summing up Dt would make the step count depend on the rounding - auto const steps{std::lround(Seconds / Dt)}; + // whole frames: summing up Dt would make their count depend on the rounding + auto const steps{std::lround(Seconds / (Dt * Track.iterations))}; for (long i = 0; i < steps; ++i) { step(Track, Dt); } @@ -253,10 +325,12 @@ void pair::find_neighbours() { void pair::step(track const &Track, double Dt) { find_neighbours(); for (auto const &placed : m_vehicles) { - placed.owner->compute_forces(Dt); + placed.owner->begin_step(); + placed.owner->forces(Dt); } for (auto &placed : m_vehicles) { - placed.position += placed.heading * placed.owner->compute_movement(Track, Dt, TLocation{placed.position, 0.0, 0.0}); + auto const &mover{placed.owner->mover()}; + placed.position += placed.heading * placed.owner->move(Track, Dt, TLocation{placed.position, 0.0, 0.0}, mover.PipePress); } } @@ -278,4 +352,24 @@ int pair::coupling() const { return m_vehicles[0].owner->mover().Couplers[m_vehicles[0].end].CouplingFlag; } +void reseed_random() { + Global.random_engine.seed(1); + Global.local_random_engine.seed(1); +} + +std::vector const &real_vehicles() { + static std::vector const paths{[] { + std::vector result; + auto const root{std::filesystem::path{EU07_UNIT_TESTS_DATA} / "fiz"}; + for (auto const &entry : std::filesystem::recursive_directory_iterator(root)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + result.push_back(std::filesystem::relative(entry.path(), root).replace_extension().generic_string()); + } + } + std::ranges::sort(result); + return result; + }()}; + return paths; +} + } // namespace test diff --git a/tests/unit/vehicle.h b/tests/unit/vehicle.h index 616e75d11..e14f4e464 100644 --- a/tests/unit/vehicle.h +++ b/tests/unit/vehicle.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,10 @@ namespace test { +// reseeds the random number engines of the simulation, as before every test case. a test making several +// vehicles in a loop reseeds before each, so each one is the same whatever comes before it +void reseed_random(); + // thrown for a test vehicle definition which can't be used: missing, malformed, or not taken by the loader class definition_error : public std::runtime_error { public: @@ -21,34 +26,59 @@ public: // definition of a test vehicle: one of the JSON files in tests/unit/data, with the sections and keys // of the game's vehicle definitions, changed by the test where it needs to: // auto heavy = definition::load("wagon").set("Param", "M", 60000).set("Brake", "BrakeValve", "ESt3"); +// or one of the game's own vehicles, converted with the files it includes to tests/unit/data/fiz (see the +// README there), which reads to the game's vehicle loader the same as the game's file: +// auto eu07 = definition::fiz("4e_v1/4e").set("Brake", "MaxBP", 3.8); +// a section of a list keeps its rows under "rows", each a list of values class definition { public: // tests/unit/data/.json static definition load(std::string const &Name); + // tests/unit/data/fiz/.json + static definition fiz(std::string const &Path); // sets the key of the section, adding either one when it's missing definition &set(std::string const &Section, std::string const &Key, nlohmann::ordered_json const &Value); - // drops the whole section, or one key of it + // drops the whole section, with its rows, or one key of it definition &remove(std::string const &Section); definition &remove(std::string const &Section, std::string const &Key); - // the definition in the form the game's vehicle loader reads + // the definition in the form the game's vehicle loader reads, the sections in their order std::string text() const; + // the vehicle type of a real definition, its file name in the game; empty for the others + std::string const &type_name() const { return m_typename; } + private: - nlohmann::ordered_json m_sections; + nlohmann::ordered_json m_sections = nlohmann::ordered_json::object(); + std::string m_typename; }; -// conditions the vehicle runs in: straight track of the given friction and gradient (rise/run) +// conditions the vehicle runs in: straight track of the given friction and gradient (rise/run), under a +// catenary of the given voltage, or without one. the track is of the vehicle's own category (rail, road, +// water), unless the test gives one, and of the standard gauge wide, unless the test gives a road's width. iterations is how many steps of the physics the game makes in a frame: +// the last one moves the vehicle fully, the ones before it (FastComputeMovement) only roughly; the game +// makes as many as it takes to keep them at 0.01 s, in fives with "full physics" set struct track { double friction{0.15}; double gradient{0.0}; + double voltage{0.0}; + int category{0}; + int iterations{1}; + double width{1.435}; }; -// one vehicle created the way TDynamicObject::Init() creates it, from the given definition +// the catenary of the Polish railways [V] +inline constexpr double catenary_3kv{3000.0}; + +class trace; + +// one vehicle created the way TDynamicObject::Init() creates it, from the given definition. Cab is the +// cab with a driver in it, 1 (front) or -1 (rear), as for a "headdriver" or "reardriver" in a scenario; +// 0 for a vehicle nobody drives. the vehicle steps the way TDynamicObject::Update() steps a lone one class vehicle { public: - explicit vehicle(definition const &Definition, double Velocity = 0.0, std::string const &Name = "test"); + explicit vehicle(definition const &Definition, double Velocity = 0.0, std::string const &Name = "test", int Cab = 0); TMoverParameters &mover() { return *m_mover; } TMoverParameters *operator->() { return m_mover.get(); } @@ -56,25 +86,35 @@ public: // keeps the brake pipe at the given pressure [bar], as a locomotive coupled to the vehicle would; // a negative value leaves the pipe alone void hold_brake_pipe(double Pressure) { m_pipepressure = Pressure; } + // keeps the main reservoir at the given pressure [bar], as a running compressor or a coupled vehicle + // feeding it would; a negative value leaves it alone + void hold_main_reservoir(double Pressure) { m_reservoirpressure = Pressure; } - // advances the physics by one step of Dt, the way the simulation loop does + // advances the physics by one frame of Track.iterations steps of Dt, the way the simulation loop does void step(track const &Track = {}, double Dt = 0.01); - // advances the physics by Seconds in steps of Dt + // advances the physics by Seconds in frames of steps of Dt void run(double Seconds, track const &Track = {}, double Dt = 0.01); + // from now on records the vehicle in the trace after every step, see golden.h + void record(trace &Trace) { m_trace = &Trace; } + + // the parts of a step, which a consist makes for all its vehicles in turn, as vehicle_table::update() + // does: the test's conditions and the constants, the forces, the movement (returning the distance) + void begin_step(); + void forces(double Dt); + double fast_move(track const &Track, double Dt, TLocation const &Location); + double move(track const &Track, double Dt, TLocation const &Location, double Trainpipe); + // vehicle speed in kilometres per hour, negative when moving backwards double speed() const { return m_mover->V * 3.6; } private: - friend class pair; - - // the two halves of a step: the forces, taken by all the vehicles in the scene first, then the movement, - // the vehicle placed at Location. returns the distance travelled [m], positive forwards - void compute_forces(double Dt); - double compute_movement(track const &Track, double Dt, TLocation const &Location); - std::unique_ptr m_mover; + int m_cab{0}; double m_pipepressure{-1.0}; + double m_reservoirpressure{-1.0}; + trace *m_trace{nullptr}; + double m_time{0.0}; // of the simulation, since the vehicle got created }; // two vehicles on a straight track, the FirstEnd of the first one facing the SecondEnd of the second one Gap metres From 939e42b611f3015f3f1bc5c1f0fe3be7b964b916 Mon Sep 17 00:00:00 2001 From: maj00r <73280030+maj00r@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:17:57 +0100 Subject: [PATCH 32/32] docs: CLAUDE.md with the team's notes for Claude Code (#258) * docs: CLAUDE.md with the team's notes for Claude Code The rules Claude Code sessions kept relearning, shared with the team: - the code builds and behaves the same with GCC 13 (Linux CI and the Sonar build) as with MSVC, with the MSVC leniencies to watch for; - every SonarCloud remark on the changed code gets fixed without changing behaviour, with the recipes for the rules that keep coming up; - building, the unit tests, scripted editor runs, the game data and the profile folders, and GPU hangs; - the game data in the asset SVN and backward compatibility: existing files keep being read as before, new entries are optional, code falls back without new assets and a change that uses them says so in its commit message, what the editor saves stays readable by the current release, new features can be turned off in eu07.ini and don't change the look of existing data abruptly; StarterNG is the official starter. * docs: CLAUDE.md on the move to OpenGL 4.6 and Vulkan betterRenderer/ is to be removed and the OpenGL 3.3 and legacy renderers are being replaced by feat/gl46-migration (PR #216, a draft with milestone 27.04). Work on that code and on the shaders is kept to fixes and to what a feature on master needs to be drawn, with the PR saying where the branch has the counterpart. --------- Co-authored-by: maj00r --- CLAUDE.md | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c1709be8d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,163 @@ +# MaSzyna: notes for Claude Code + +## The code builds and works with GCC, not only MSVC + +The Linux CI (`.github/workflows/build.yml`) builds with GCC 13 on ubuntu-24.04, through the Sonar build-wrapper, and `sonar.yml` +analyses that build once it has passed. A change that compiles with MSVC only fails there, and a failed GCC build also leaves Sonar +without an analysis of the PR. So every change has to compile with GCC, and behave the same with it, as well as with MSVC. The CI +builds with `-DWITH_BETTER_RENDERER=OFF` and Sonar excludes `betterRenderer/`, so that part is neither built on CI nor analysed. + +Where no GCC is at hand, read the new code for what MSVC lets through and GCC doesn't: + +- `constexpr` initialised from a floating or class-type constant declared only `const`: make the source `constexpr` too. +- Standard headers MSVC pulls in through others: include them yourself. For example, `` for `std::views`, ``, + ``, ``, ``, `` and ``. +- `typename` and `this->` where a template needs them. +- A temporary bound to a non-const reference. +- Explicit specialisations inside a class. +- A name a class uses and then declares again in its own body (GCC: "changes meaning"). +- A nested type with default member initialisers used as a default argument inside the class that encloses it. +- A declaration changed in a header (`const`, parameters) without its definition, or a new member function defined but not declared. +- `std::ifstream` / `std::ofstream` opened with a `std::wstring`. +- MSVC-only functions: `_stricmp`, `sprintf_s`, `fopen_s`, `localtime_s` and the like. +- Designated initialisers out of declaration order. +- `emplace_back(a, b, c)` with parenthesised aggregate initialisation is fine on GCC 13, but not for `std::array`. There is no brace + elision in parentheses, so build a named local instead. + +The same behaviour needs portable code. Don't rely on the order arguments are evaluated in, on the signedness of `char`, on the size of +`wchar_t`, or on `long` being 32 bits. + +## SonarCloud: every remark on the changed code gets fixed + +SonarCloud (`sonar.yml`, `sonar-project.properties`) analyses each PR. A PR counts as finished only when every remark Sonar raises on +the code it changes is fixed. That means the code smells too, not just the bugs, the vulnerabilities and the quality gate. The analysis +runs on the GCC build, so a PR that fails to build with GCC gets no analysis at all. + +Fixing a remark must not change what the code does: + +- Keep the order of side effects, the conditions and the floating-point results as they were. +- Text in `STR(...)`, `STR_C(...)` and `_(...)` is looked up in the translations (`lang/*.po`), so it stays byte for byte the same. + The same goes for what is written to the log. +- `// NOSONAR: ` is only for a real false positive, or where a fix would change behaviour. Use it rarely, always with the + reason. + +Write new code so that it doesn't raise the rules that keep coming up: + +- **S134** (nesting deeper than 3 `if` / `for` / `while` / `switch` / `try`) and **S3776** (cognitive complexity over 25): + - use early `continue` / `return`; + - move loop bodies and branches into named helpers, preferably free functions in the anonymous namespace; + - group the state of a long algorithm in a small struct with methods. + + Every `else if`, every `&&` / `||` sequence and every nested lambda adds to the complexity. +- **S1188**: a lambda over 20 lines becomes a named function. +- **S107**: a function with more than 7 parameters takes a struct. +- **S3608**: lambdas capture explicitly (`[this, &points]`, not `[&]` or `[=]`). The only exception is `[&]` on a lambda passed + straight to a standard algorithm; `[=]` is flagged even there, and so is `[&]` passed to one of our own functions. +- **S6197** / **S5566**: use the `std::ranges::` algorithm that takes the container rather than a begin/end pair or a hand-written + loop. +- **S6165**: use `std::erase_if` rather than erase-remove. +- **S6004**: a variable used only by the condition is declared in the `if (init; condition)`. +- **S3358**: no nested `?:`; use an `if` or a small function. +- **S1121**: no assignment inside an expression, chained ones included (`a = b = c`). +- **S1117**: no declaration shadowing a local, a member or an enumerator. +- **S6003**: `v.emplace_back(a, b, c)` rather than `v.push_back({a, b, c})`. For a `std::array` element, push a named local instead + (see the GCC notes above). +- **S6012**: let class template argument deduction work: `std::vector values{1.0, 2.0}`. +- **S5945**: `std::array` / `std::vector` / `std::string`, not C arrays. +- **S1659**: one declaration per statement. +- **S6009**: `std::string_view` rather than `std::string const &` for a string parameter that is only read. +- **S6185** / **S6494**: `std::format` rather than concatenation or `snprintf`. +- **S924**: at most one `break` per loop. +- **S886**: a `for` whose counter changes in its body becomes a `while`. +- **S5997**: `std::scoped_lock` rather than `std::lock_guard`. +- **S6168**: `std::jthread` rather than `std::thread`. A `std::jthread` joins in its destructor where a `std::thread` would + terminate, so in existing code switch only where the thread is always joined or detached before it is destroyed. +- **S6177**: `using enum` where a function names many enumerators. +- **S5350** / **S995**: `const` on references and pointers that aren't written through. +- **S5817**: `const` on member functions that change nothing. +- **S2193**: no `float` loop counters. +- **S5421**: no non-const globals. +- **S6179**: `std::lerp` rather than a hand-written interpolation, in new code. In existing code only where it gives the very same + result. +- **S6164**: `std::numbers::pi` rather than a literal, in new code. In existing code only where it gives the very same result. + +The project is C++20 (`CMAKE_CXX_STANDARD 20`), so no C++23 library addition is available on any compiler. That rules out +`std::ranges::to`, `views::zip`, `views::enumerate`, `ranges::contains`, `std::print` and `std::expected`. + +## Renderers: the move to OpenGL 4.6 and Vulkan + +The renderers on `master` are to be replaced. `feat/gl46-migration` (PR #216, a draft with milestone 27.04, to be merged after the +26.11 release) replaces the OpenGL 3.3 renderer with an OpenGL 4.6 core one and adds a Vulkan 1.4 renderer beside it. It also +removes the legacy OpenGL 2.1, the OpenGL ES / ANGLE and the NVRHI (`betterRenderer/`) paths. + +- `betterRenderer/` will be removed. Don't put work into it. +- On that branch, of the `rendering/opengl*` files only `openglcamera.*` and `openglmatrixstack.*` stay. The whole + `rendering/opengl33*` family and the rest of the legacy `rendering/opengl*` go, and the new renderers are in + `rendering/opengl46/`, `rendering/vulkan14/` and `rendering/common/`. The shared `rendering/renderer.*`, `geometrybank.*`, + `particles.*` and the like stay. The shaders move from `shaders/` to `shaders/ogl46/` and `shaders/vk14/`. +- So a change on `master` to the code that goes away, or to the shaders, may be pointless. Keep it to fixes and to what a feature + on `master` needs in order to be drawn at all. For such a change, say in the PR where `feat/gl46-migration` has the counterpart, + or that the branch already carries the change. + +## Building, testing and running + +- The unit tests (`tests/unit`) run at the end of the build, which prints `Unit tests: N/N passed` and fails on any failed case. + `-DWITH_TESTS=OFF` builds the game without them. +- A new feature goes on its own branch from `master`, not on whatever branch is checked out. Say which branch the work lands on + before the first edit. A fresh worktree has empty submodules, so it doesn't build until they are filled. +- The editor runs a script when `EU07_EDITOR_SELFTEST=` is set: one command per line, `#` for comments. The commands are in + `application/editormodeselftest*.cpp` (`camera`, `wait`, `select`, `lay`, `screenshot`, `save`, `quit` and many more). + - Run it on a copy of the scenery, never on the original, and delete the copy afterwards. + - Give every run a timeout and keep an eye on its memory. A loop gone wrong can fill the RAM of the whole machine. + - `screenshot` only queues the grab and waits 3 frames; the image is written later on another thread. A click right after it + has been seen to lose the selection, so leave a `wait` of about 15 frames after a `screenshot` before clicking. +- At run time the editor's texts load from `lang/editor_.po` in the game folder (`utilities/translation.cpp`), not from + the build. New strings of a branch show in English until that file has them. Without the `fonts` folder Polish letters show as `?`. +- When a build runs on a copy of the game data made of links to the real folders, turn off whatever writes into the data + (`convertmodels 0` in `eu07.ini`), or the run changes the real game folder through the links. +- A test of code that moves, migrates or deletes files in the user's profile (`%APPDATA%`, `%LOCALAPPDATA%`, caches) gets its own + test folder. Never point it at the real profile. +- A GPU hang (Windows TDR) can freeze the whole machine, not just the game. Don't test a hang hypothesis by running it. Guard the + code against what causes it (degenerate geometry, NaN in the camera or in transforms) and read the `LiveKernelEvent` / watchdog + dumps instead. +- PowerShell variable names are case-insensitive: `$Track` and `$track` are the same variable. + +## Game data and backward compatibility + +The code and the game data live apart. The assets (sceneries, vehicles with their `.fiz` and `.mmd`, models, textures, sounds, +timetables, most of `lang` and the shipped `eu07.ini`) are in a separate SVN repository, `https://svn.eu07.pl/pctga`. It needs an +account; there is no anonymous checkout. Of `lang/`, only `editor_pl.po` is in this repository. Players install and +launch the game through Steam, GOG and the like, and run new builds on the data they already have. + +The official starter is StarterNG (`https://github.com/MaSzyna-EU07/StarterNG`), a separate repository; the older +`MaSzyna-EU07/Starter` is not the one in use. `launcher/` in this repository is the simulator's built-in scenario and vehicle +picker (`launcher_mode`), not StarterNG. + +- Every existing file must still be read the way it was. Don't rename or drop a keyword, change the meaning or the unit of a value, + or make an optional entry required. This holds for every format the game reads: for example `.scn` and its includes, `.fiz`, + `.mmd`, `.e3d` / `.t3d`, `.mat`, the `.txt` timetables and track profiles, `eu07.ini` and the Python screens. +- A bug fix may change what an existing file leads to, such as how a vehicle from its `.fiz` behaves, as long as the PR says so. +- Something new in a format is optional, and without it the code does exactly what it did before. A file that loaded correctly + before doesn't get new warnings or errors in the log. A real fault that is newly detected may be logged. +- A code change must not need a change in the SVN to work. Where it uses new or changed assets, it falls back to the old behaviour + without them, and the PR says what is to be added to the SVN and in what order the two go in. +- A change that uses new or changed assets (and falls back without them, see above) says so plainly in its commit message: which + files, and what is to change in them. It is never left implied. Before such a commit, either make the asset change right away in + the user's SVN working copy, or get the user's agreement to leave it for later. Ask the user where their SVN working copy is; the + installed game folder is not one. Pushing to git and committing to the SVN both need the user's consent. +- A scenery saved by the editor without using a new feature holds nothing the current release can't read. The entries of a new + feature (such as a new kind of node) appear only where that feature was used, and the PR says that the release doesn't read + them. +- Saving patches the scenery files by byte spans: unchanged definitions stay byte for byte, and an edited node is written anew by + its `export_as_text`. A change to saving must not widen what gets rewritten, nor make `export_as_text` drop more of an edited + node (comments, unknown tokens) than it did before. +- A new feature that changes what the simulator does or shows with existing sceneries and vehicles can be turned off in + `eu07.ini`, and turned off, the simulator behaves as it did before the feature. Editor tools and new optional entries of a + format are covered by the points above. The key is parsed in `utilities/Globals.cpp` (`global_settings::ConfigParse*`) and + written by `global_settings::export_as_text`, which puts the settings in the log at start. `eu07.ini` is read from the user's + profile (`user_config_path`), and from the game folder only when the profile has none; test the switch in the file that is + actually read. +- A feature doesn't make existing sceneries and vehicles look suddenly different after an update. A change to how things look is + something the player turns on, or comes in gradually. +- When a format really has to change, read both the old and the new form, and say in the PR what changes for the data and for + the people who make it.