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/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. diff --git a/CMakeLists.txt b/CMakeLists.txt index 477f584e9..6f34ad61f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/TrackUneven.cpp" "world/Road.cpp" "world/Sweep.cpp" "world/RoadPoint.cpp" @@ -462,6 +463,8 @@ set(CORE_SOURCES "utilities/utilities.cpp" "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/McZapkie/MOVER.h b/McZapkie/MOVER.h index 29b3e75cd..199c65a0c 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 @@ -2423,11 +2437,47 @@ 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); 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 2b2275112..20761bacb 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -16,6 +16,9 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/parser.h" #include "simulation/simulation.h" + +#include +#include //--------------------------------------------------------------------------- // Ra: tu należy przenosić funcje z mover.pas, które nie są z niego wywoływane. @@ -49,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); } } @@ -1176,6 +1184,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 +1250,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 +1285,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 +1312,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 +4384,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 +4409,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 +4439,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 +5056,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 +5080,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) { @@ -5088,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; ratio = Hamulec->GetBrakeRatio(ratio); @@ -5287,7 +5343,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 @@ -5400,25 +5456,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; @@ -6851,13 +6905,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))) { @@ -10430,7 +10483,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); @@ -10469,6 +10525,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, "")) { @@ -10647,12 +10704,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) @@ -10685,7 +10745,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 { @@ -10781,6 +10841,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}}; @@ -10940,14 +11010,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; } } @@ -11340,6 +11411,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/application.cpp b/application/application.cpp index 119cb021e..9b7b82853 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(250)); 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 = [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) 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/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); 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/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..c43670ca4 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; } @@ -1159,7 +1161,39 @@ void editor_mode::render_path_parameters(TTrack &Track) finish_edit(rebuild::none); ImGui::InputFloat(STR_C("Sound distance"), &track->fSoundDistance, 0.0f, 0.0f, "%.1f"); item_tooltip("Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default"); - finish_edit(rebuild::none); + finish_edit(rebuild::parameters); + if ((track->eType == tt_Normal || track->eType == tt_Switch) && (track->iCategoryFlag & 15) == 1) + { + if (ImGui::InputFloat(STR_C("Unevenness (mm)"), &track->m_uneven.value, 0.0f, 0.0f, "%.1f")) + 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); + 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) + { + if (ImGui::InputFloat(STR_C("Dip at rail joints (mm)"), &track->m_uneven.dip, 0.0f, 0.0f, "%.1f")) + track->m_uneven.dip = track->m_uneven.dip < 0.f ? -1.f : std::min(track->m_uneven.dip, track_uneven::most); + item_tooltip("Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness"); + finish_edit(rebuild::parameters); + } ImGui::InputInt(STR_C("Quality"), &track->iQualityFlag, 0); item_tooltip("Quality of the track, 20 is the nominal one; lower values give a rougher ride"); finish_edit(rebuild::none); @@ -1188,8 +1222,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 +1272,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/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) { diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index dbdce6068..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(); } @@ -41,11 +43,26 @@ 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); + } + // 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(); + Application.push_mode(eu07_application::mode::launcher); + return true; + } } try { @@ -53,8 +70,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 2dd771bdd..5443d158c 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -395,14 +395,21 @@ 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; 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; @@ -567,7 +574,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 +605,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 +715,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/editor/editorTrack.cpp b/editor/editorTrack.cpp index 418d908fe..8e6339913 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -152,6 +152,7 @@ editor_track::state editor_track::capture(TTrack const &Track) for (auto const *isolated : Track.Isolated) result.isolated.push_back(isolated->asName); result.overhead = Track.fOverhead; + result.uneven = Track.m_uneven; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; result.sleeper_model = Track.m_sleeper_model_name; @@ -250,6 +251,7 @@ void editor_track::apply(TTrack &Track, state const &State) Track.fTexSlope = State.texslope; isolated(Track, State.isolated); overhead(Track, State.overhead); + Track.m_uneven = State.uneven; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; Track.m_sleeper_model_name = State.sleeper_model; @@ -424,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; @@ -455,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) @@ -476,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(); @@ -2346,10 +2351,11 @@ void write_points(std::ostream &Text, segment_data const &Path) Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; } -void write_ending(std::ostream &Text, TTrack const &Style) +void write_ending(std::ostream &Text, TTrack const &Style, bool const Switch) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; + Text << track_uneven::write_attribute(Style.m_uneven, Switch); // a piece of the track made from another one stays in its track circuit and under the same overhead line for (auto const *isolated : Style.Isolated) Text << "isolated " << isolated->asName << ' '; @@ -2401,7 +2407,7 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector write_points(text, Paths[1]); if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle) text << "trackbed " << material_name(trackbed) << ' '; - write_ending(text, Style); + write_ending(text, Style, true); track = load_path(text.str(), Style, Name); } track->m_paths.assign(Paths.begin(), Paths.begin() + 2); @@ -2662,7 +2668,7 @@ TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) write_points(text, Path); if (auto const speed{velocity(Style)}; speed > 0.0) text << "velocity " << speed << ' '; - write_ending(text, Style); + write_ending(text, Style, false); auto *track{load_path(text.str(), Style)}; track->m_paths = {Path}; return track; @@ -2816,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 43d42762d..efb5fdac9 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -17,6 +17,7 @@ http: #include #include "world/Segment.h" +#include "world/TrackUneven.h" #include "utilities/Classes.h" class TTrack; @@ -55,6 +56,7 @@ class editor_track float texslope{0.9f}; std::vector isolated; float overhead{-1.f}; + track_uneven::attribute uneven; bool sleepers{false}; float sleeper_frequency{0.6f}; std::string sleeper_model; @@ -91,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); @@ -348,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/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/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') 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/lang/editor_pl.po b/lang/editor_pl.po index 58b301095..dbf7c124e 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -861,6 +861,39 @@ msgstr "Odstęp stuków" msgid "Quality" msgstr "Jakość" +msgid "Unevenness (mm)" +msgstr "Nierówności toru (mm)" + +msgid "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" +msgstr "Największa odchyłka toku szynowego na cięciwie 10 m. Limity Id-14: 6 mm przy 160 km/h, 10 przy 120, 18 przy 80, 35 przy 40.\nPrzy podanym odstępie styków szyny zapadają się też na stykach. Połączone rozjazdy mają najmniejszą ze swoich wartości" + +msgid "Up and down (mm)" +msgstr "W pionie (mm)" + +msgid "Sideways (mm)" +msgstr "W poziomie (mm)" + +msgid "Cross level (mm)" +msgstr "Przechyłka (mm)" + +msgid "Diverging path (mm)" +msgstr "Tor na bok (mm)" + +msgid "Diverging, up and down (mm)" +msgstr "Na bok, w pionie (mm)" + +msgid "Diverging, sideways (mm)" +msgstr "Na bok, w poziomie (mm)" + +msgid "Set apart from the unevenness, mm. Negative: the unevenness" +msgstr "Osobno od nierówności, mm. Ujemna: wartość nierówności" + +msgid "Dip at rail joints (mm)" +msgstr "Zapadnięcie na stykach szyn (mm)" + +msgid "Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness" +msgstr "Głębokość zapadnięcia na każdym styku szyn, co odstęp styków. Działa też przy nierównościach 0. Ujemna: 0,4 nierówności" + msgid "Damage flags" msgstr "Flagi uszkodzeń" diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index d95c962b5..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") @@ -23,7 +45,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 +139,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,6 +305,7 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) draw_droptarget(trainset, position++); ImGui::SameLine(15.0f); + // 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; @@ -332,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(); @@ -340,41 +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; - for (auto &s : ptr->vehicle.lock()->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(); +} - 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->skin + "_" + 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 bcd18ac90..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) { @@ -10,13 +12,22 @@ 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::system_error const &) { + // a name outside of the code page, which the path can't give as a string on Windows + 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)) diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 770790c1e..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; } @@ -514,6 +516,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 +554,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 +784,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/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/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/model/material.cpp b/model/material.cpp index dd7598ab1..4e96ddd8c 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -662,12 +662,176 @@ 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.glossiness = Source.glossiness; + variant.name = Source.name + Suffix; + variant.size = Source.size; + variant.is_good = Source.is_good; + variant.path = Source.path; + 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( param_confs const &Conf, std::string const &Name ) { + + auto const own { Conf.find( Name ) }; + if( own == Conf.end() ) { + return false; + } + 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 + && 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; + } + 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 ]; + } + std::erase_if( + Variant.params_state, + [ &entry ]( gl::shader::param_entry const &State ) { + return State.location == entry->second.location && State.offset == entry->second.offset; } ); + } +} + +// 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" ) }; + 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::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 }; + std::erase_if( + Variant.params_state, + [ location ]( gl::shader::param_entry const &State ) { + return State.location == location; } ); + return true; +} + +} // namespace + +material_handle +material_manager::create_matte( material_handle const Material ) { + + if( Material == null_handle || Material >= m_materials.size() ) { + return Material; + } + if( auto const lookup { m_mattes.find( Material ) }; lookup != m_mattes.end() ) { + return lookup->second; + } + auto matte { variant_of( m_materials[ Material ], ":matte" ) }; + make_matte( matte ); + 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; + } + auto tinted { variant_of( m_materials[ Material ], ":tinted" ) }; + if( false == make_tinted( tinted, Colour ) ) { + return Material; + } + 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 @@ -676,6 +840,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 f329a3256..82d14557c 100644 --- a/model/material.h +++ b/model/material.h @@ -142,6 +142,13 @@ 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 ); + // 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 & material( material_handle const Material ) const { return m_materials[ Material ]; } opengl_material & @@ -161,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 @@ -170,6 +180,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/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/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index f2db8b8bf..aa7c1f8b5 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,15 @@ 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 { + + if( ( Geometry.chunk == 0 ) || ( Geometry.chunk > m_chunks.size() ) ) { return no_primitive; } + + 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 +441,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 +514,15 @@ 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 { + + if( ( Geometry.bank == 0 ) || ( Geometry.bank > m_geometrybanks.size() ) ) { return geometry_bank::no_primitive; } + + 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..c01e46fe3 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,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...; 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: @@ -220,6 +224,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 +243,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..c24f750c8 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; @@ -2201,8 +2205,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 +2227,12 @@ 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 +{ + auto const primitive{m_geometry.primitive(Geometry)}; + return primitive == gfx::geometry_bank::no_primitive ? -1 : static_cast(primitive); +} + // material methods material_handle opengl33_renderer::Fetch_Material(std::string const &Filename, bool const Loadnow) { @@ -2225,6 +2240,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); @@ -2639,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); } @@ -2653,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); } @@ -2844,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); @@ -3151,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 }; @@ -3163,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 ) { @@ -3183,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 ) { @@ -4036,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"); @@ -4186,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) { @@ -5410,6 +5551,13 @@ void opengl33_renderer::Pick_Node_Callback(std::functionis_busy()) diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 64574420f..1b4897386 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 * @@ -108,6 +115,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 @@ -293,12 +302,19 @@ 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); 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 59152c64d..180dba771 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; @@ -1758,6 +1762,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 { @@ -1888,6 +1897,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::ranges::replace_if( m_picksceneryitems, [Node]( scene::basic_node const *Item ) { return Item == Node; }, nullptr ); +} + void opengl_renderer::Render( scene::basic_region *Region ) { @@ -2776,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 ) { @@ -3094,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 78fffa40b..3a3538a6c 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; @@ -103,6 +104,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 @@ -234,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 diff --git a/rendering/particles.cpp b/rendering/particles.cpp index 5f7158e0c..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,53 +163,32 @@ smoke_source::update( double const Timedelta, bool const Onlydespawn ) { glm::dvec3{ std::numeric_limits::max() }, glm::dvec3{ std::numeric_limits::lowest() } }; - m_spawncount = ( - ( ( false == Global.Smoke ) || ( true == Onlydespawn ) ) ? - 0.f : - std::min( - static_cast(m_spawncount + ( m_spawnrate * Timedelta * Global.SmokeFidelity )), - static_cast(m_max_particles) ) ); - // consider special spawn rate cases + auto const spawning { ( true == Global.Smoke ) && ( false == Onlydespawn ) }; + 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 = - ((false == Global.Smoke) || (true == Onlydespawn)) ? - 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 = - ((false == Global.Smoke) || (true == Onlydespawn)) ? - 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 = - ((false == Global.Smoke) || (true == Onlydespawn)) ? - 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/rendering/renderer.h b/rendering/renderer.h index 0b9eb60f4..1a110e689 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -59,16 +59,26 @@ 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*/ ) { + // 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 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. @@ -90,6 +100,10 @@ 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*/ ) { + // 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/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; 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. 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/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) diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 1ff771d1d..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,7 +543,14 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } else if( nodedata.type == "track" ) { - auto *path { deserialize_path( Input, Scratchpad, nodedata ) }; + 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/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 bca46e83b..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,22 +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. Coupled vehicles - 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..4af8be18a --- /dev/null +++ b/tests/unit/scenario.cpp @@ -0,0 +1,100 @@ +#include "stdafx.h" +#include "scenario.h" + +#include "McZapkie/hamulce.h" + +namespace test { + +namespace { + +// whether the pressure switch of a vehicle in the train still cuts the start off: a half of a locomotive whose +// switch is on when the controller leaves zero doesn't close its line contactors until the controller is back at zero +bool start_cut_off(consist &Train) { + for (std::size_t i = 0; i < Train.size(); ++i) { + if (Train[i].mover().ControlPressureSwitch) { + return true; + } + } + return false; +} + +} // namespace + +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}; + // as a driver does, wait for the brakes to let go far enough for every pressure switch of the train to allow the + // start; otherwise whether all the halves of a locomotive start hangs on thousandths of a bar of their cylinders + for (int i = 0; i < 120 && start_cut_off(m_train); ++i) { + wait(1.0); + } + 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_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/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_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_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_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.cpp b/tests/unit/test_couplers.cpp new file mode 100644 index 000000000..2a0ff6e50 --- /dev/null +++ b/tests/unit/test_couplers.cpp @@ -0,0 +1,527 @@ +#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); + } +} + +// 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); +} 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_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_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_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_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_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_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_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_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/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_track_uneven.cpp b/tests/unit/test_track_uneven.cpp new file mode 100644 index 000000000..b55de1644 --- /dev/null +++ b/tests/unit/test_track_uneven.cpp @@ -0,0 +1,336 @@ +#include "stdafx.h" +#include "doctest.h" +#include "world/TrackUneven.h" + +#include +#include +#include +#include + +namespace { + +double deviation(std::vector const &Values) +{ + auto mean{0.0}; + for (auto const value : Values) + mean += value; + 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 / count); +} + +double peak(std::vector Values) +{ + for (auto &value : Values) + value = std::abs(value); + std::ranges::sort(Values); + return Values[Values.size() * 999 / 1000]; +} + +void add_chord(std::vector &Output, std::vector const &Line, int const Half) +{ + for (int i = Half; i + Half < static_cast(Line.size()); ++i) + Output.push_back(Line[i] - 0.5 * (Line[i - Half] + Line[i + Half])); +} + +} // 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 + auto const amplitude{track_uneven::stray_per_mm * 18.0 * 1000.0}; // mm + double const gauge{1.435}; + double const step{0.5}; + int const steps{4000}; + int const base{10}; // 5 m + int const lines{36}; + 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::array, 2> up; + std::array, 2> side; + std::vector middle; + for (int i = 0; i < steps; ++i) + { + auto const point{start + along * (i * step)}; + for (int rail = 0; rail < 2; ++rail) + { + auto const sign{rail == 0 ? 1.0 : -1.0}; + auto const stray{track_uneven::field(point + across * (sign * 0.5 * gauge))}; + up[rail].push_back((stray.y - sign * 0.5 * track_uneven::level(point, across)) * amplitude); + side[rail].push_back(glm::dot(stray, across) * amplitude); + } + middle.push_back(track_uneven::field(point).y * amplitude); + } + 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); + add_chord(horizontals, side[rail], base); + } + for (int i = 0; i + base < steps; ++i) + twists.push_back((up[0][i + base] - up[1][i + base]) - (up[0][i] - up[1][i])); + for (int i = 0; i < steps; ++i) + { + gauges.push_back(side[0][i] - side[1][i]); + levels.push_back(up[0][i] - up[1][i]); + } + vertical += peak(verticals) / lines; + horizontal += peak(horizontals) / lines; + twist += peak(twists) / lines; + wander += peak(gauges) / lines; + level += peak(levels) / lines; + away += peak(middle) / lines; + j += (deviation(verticals) + deviation(horizontals) + deviation(twists) + 0.5 * deviation(gauges)) / 3.5 / lines; + } + CHECK(vertical == doctest::Approx(18.0).epsilon(0.1)); + CHECK(horizontal == doctest::Approx(17.0).epsilon(0.2)); + CHECK(twist == doctest::Approx(16.0).epsilon(0.2)); + CHECK(wander == doctest::Approx(10.0).epsilon(0.2)); + CHECK(level == doctest::Approx(20.0).epsilon(0.1)); + CHECK(j == doctest::Approx(5.3).epsilon(0.2)); + CHECK(away < 40.0); // the long waves go past the 15 mm off the survey marks of appendix 1a +} + +TEST_CASE("uneven track: a place strays the same every time") +{ + // no random numbers: changing these changes how every uneven track in every scenery looks + glm::dvec3 const place{1234.5, 0.0, -678.9}; + auto const stray{track_uneven::field(place)}; + CHECK(stray.x == doctest::Approx(0.309777548158).epsilon(1e-9)); + CHECK(stray.y == doctest::Approx(0.311350299745).epsilon(1e-9)); + CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); + CHECK(track_uneven::level(place, {1.0, 0.0, 0.0}) == doctest::Approx(0.53 * -0.178598957983).epsilon(1e-9)); + CHECK(track_uneven::level(place, {0.0, 0.0, 1.0}) == doctest::Approx(0.53 * -0.868291889902).epsilon(1e-9)); +} + +TEST_CASE("uneven track: tracks laid towards each other meet at one cant") +{ + // the same rail stays lower seen from either of them, and a path taken the other way sees the roll turned over + glm::dvec3 const place{812.25, 0.0, 3071.5}; + glm::dvec3 const across{0.6, 0.0, -0.8}; + CHECK(track_uneven::level(place, -across) == doctest::Approx(-track_uneven::level(place, across))); + CHECK(track_uneven::roll(place, -across, 1.435) == doctest::Approx(-track_uneven::roll(place, across, 1.435))); + // and there's no step along a curve turning through the headings + auto last{track_uneven::level(place, {1.0, 0.0, 0.0})}; + for (int step = 1; step <= 3600; ++step) + { + auto const heading{glm::two_pi() * step / 3600.0}; + auto const value{track_uneven::level(place, {std::cos(heading), 0.0, std::sin(heading)})}; + REQUIRE(std::abs(value - last) < 0.002); + last = value; + } +} + +TEST_CASE("uneven track: the field stays within its bound") +{ + auto most{0.0}; + auto highest{0.0}; + for (int i = 0; i < 20000; ++i) + { + auto const stray{track_uneven::field({i * 3.7 - 30000.0, 0.0, i * -2.9 + 41000.0})}; + most = std::max({most, std::abs(stray.x), std::abs(stray.z)}); + highest = std::max(highest, std::abs(stray.y)); + } + CHECK(most <= 1.0); + CHECK(most > 0.5); + CHECK(highest <= 1.69); + CHECK(highest > 0.5); +} + +TEST_CASE("uneven track: all the values in one attribute") +{ + auto const one{track_uneven::read_fields<6>("18")}; + CHECK(one[0] == doctest::Approx(18.f)); + CHECK(one[1] < 0.f); + auto const both{track_uneven::read_fields<6>("14;35")}; + CHECK(both[0] == doctest::Approx(14.f)); + CHECK(both[1] == doctest::Approx(35.f)); + CHECK(both[2] < 0.f); + auto const gaps{track_uneven::read_fields<6>("18.5;;;25;6")}; + CHECK(gaps[0] == doctest::Approx(18.5f)); + CHECK(gaps[1] < 0.f); + CHECK(gaps[2] < 0.f); + CHECK(gaps[3] == doctest::Approx(25.f)); + CHECK(gaps[4] == doctest::Approx(6.f)); + auto const bounds{track_uneven::read_fields<6>("500;-3;x")}; + CHECK(bounds[0] == doctest::Approx(track_uneven::most)); + CHECK(bounds[1] == doctest::Approx(0.f)); + CHECK(bounds[2] < 0.f); + CHECK(track_uneven::write_fields<6>({14.f, 35.f, -1.f, -1.f, -1.f, -1.f}) == "14;35"); + CHECK(track_uneven::write_fields<6>({18.5f, -1.f, -1.f, 25.f, 6.f, -1.f}) == "18.5;;;25;6"); + CHECK(track_uneven::write_fields<6>({0.f, -1.f, 30.f, -1.f, -1.f, -1.f}) == "0;;30"); + CHECK(track_uneven::write_fields<6>({18.f, -1.f, -1.f, -1.f, -1.f, -1.f}) == "18"); +} + +TEST_CASE("uneven track: the attribute goes by the entry it's written into") +{ + auto const plain{track_uneven::read_attribute("18;22;19;25;6", false)}; + CHECK(track_uneven::write_attribute(plain, false) == "uneven 18;22;19;25;6 "); + // a switch made in the style of a plain track: no path p3-p4 of its own, the components in their places + CHECK(track_uneven::write_attribute(plain, true) == "uneven 18;;22;19 "); + auto const made{track_uneven::read_attribute("18;;22;19", true)}; + CHECK(track_uneven::declared(made, false, 0).up == doctest::Approx(22.0)); + CHECK(track_uneven::declared(made, false, 0).side == doctest::Approx(19.0)); + CHECK(track_uneven::declared(made, false, 1).up == doctest::Approx(22.0)); + CHECK(track_uneven::declared(made, false, 0).cross == doctest::Approx(0.0)); + // a switch with its own values, and a plain track in its style + auto const points{track_uneven::read_attribute("14;35;16;17", true)}; + CHECK(track_uneven::write_attribute(points, true) == "uneven 14;35;16;17 "); + CHECK(track_uneven::declared(points, false, 1).up == doctest::Approx(35.0)); + CHECK(track_uneven::declared(points, false, 1).side == doctest::Approx(35.0)); + CHECK(track_uneven::write_attribute(points, false) == "uneven 14;16;17 "); + // a plain track: the value stands in for what's left out, only there is the cross level + auto const track{track_uneven::read_attribute("18;;30", false)}; + CHECK(track_uneven::declared(track, true, 0).up == doctest::Approx(18.0)); + CHECK(track_uneven::declared(track, true, 0).side == doctest::Approx(30.0)); + CHECK(track_uneven::declared(track, true, 0).cross == doctest::Approx(18.0)); + // nothing given, nothing written + CHECK(track_uneven::write_attribute({}, false).empty()); + CHECK(track_uneven::write_attribute({}, true).empty()); + // a value of 0 stays when there's more after it + CHECK(track_uneven::write_attribute(track_uneven::read_attribute("0;;;;6", false), false) == "uneven 0;;;;6 "); +} + +TEST_CASE("uneven track: each component comes to its neighbours along a track") +{ + double const length{500.0}; + auto const atstart{track_uneven::along(track_uneven::amounts{18.0, 16.0, 14.0}, {5.0, 4.0, 3.0}, {7.0, 8.0, 9.0}, 0.0, length)}; + CHECK(atstart.up == doctest::Approx(5.0)); + CHECK(atstart.side == doctest::Approx(4.0)); + CHECK(atstart.cross == doctest::Approx(3.0)); + auto const inside{track_uneven::along(track_uneven::amounts{18.0, 16.0, 14.0}, {5.0, 4.0, 3.0}, {7.0, 8.0, 9.0}, 0.5 * length, length)}; + CHECK(inside.up == doctest::Approx(18.0)); + CHECK(inside.side == doctest::Approx(16.0)); + CHECK(inside.cross == doctest::Approx(14.0)); +} + +TEST_CASE("uneven track: axles don't stray along the track") +{ + glm::dvec3 const across{0.6, 0.0, -0.8}; + glm::dvec3 const along{0.8, 0.0, 0.6}; + auto const moved{track_uneven::across_only(across * 0.03 + along * 0.05 + glm::dvec3{0.0, 0.02, 0.0}, across)}; + CHECK(glm::dot(moved, along) == doctest::Approx(0.0)); + CHECK(glm::dot(moved, across) == doctest::Approx(0.03)); + CHECK(moved.y == doctest::Approx(0.02)); +} + +TEST_CASE("uneven track: a path of no length gives no vector across") +{ + glm::dvec3 const none{0.0}; + CHECK(track_uneven::across_of(none, none) == none); + CHECK(glm::length(track_uneven::across_of({3.0, 0.0, 4.0}, none)) == doctest::Approx(1.0)); + // it can't pass for a joint: the check of the two vectors being alike fails + CHECK(glm::dot(track_uneven::across_of(none, none), glm::dvec3{1.0, 0.0, 0.0}) < 0.9); + // falls back on the chord + auto const chord{track_uneven::across_of(none, {0.0, 0.0, 10.0})}; + CHECK(chord.x == doctest::Approx(-1.0)); + CHECK(chord.z == doctest::Approx(0.0)); + // straight up has no direction in the plan + CHECK(glm::length(track_uneven::across_of({0.0, 5.0, 0.0}, {0.0, 0.0, 10.0})) == doctest::Approx(1.0)); +} + +TEST_CASE("uneven track: the cant at a joint") +{ + CHECK(track_uneven::joint_roll(0.1f, -0.1f, true, true) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.2f, 0.4f, false, false) == doctest::Approx(0.3f)); + CHECK(track_uneven::joint_roll(0.1f, 0.0f, true, false) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.0f, 0.1f, false, true) == doctest::Approx(0.f)); +} + +TEST_CASE("uneven track: amounts take the lowest of each component") +{ + auto const least{track_uneven::least({14.0, 9.0, 0.0}, {7.0, 35.0, 3.0})}; + CHECK(least.up == doctest::Approx(7.0)); + CHECK(least.side == doctest::Approx(9.0)); + CHECK(least.cross == doctest::Approx(0.0)); + // the diverging path of a switch: from the common end at the lowest to its own value at the far end + double const length{30.0}; + CHECK(track_uneven::along(35.0, 7.0, 35.0, 0.0, length) == doctest::Approx(7.0)); + CHECK(track_uneven::along(35.0, 7.0, 35.0, length, length) == doctest::Approx(35.0)); +} + +TEST_CASE("uneven track: the value along a track meets its neighbours exactly") +{ + double const length{500.0}; + CHECK(track_uneven::along(20.0, 5.0, 7.0, 0.0, length) == doctest::Approx(5.0)); + CHECK(track_uneven::along(20.0, 5.0, 7.0, length, length) == doctest::Approx(7.0)); + CHECK(track_uneven::along(20.0, 5.0, 7.0, 0.5 * length, length) == doctest::Approx(20.0)); + // equal values stay constant + CHECK(track_uneven::along(20.0, 20.0, 20.0, 3.0, length) == doctest::Approx(20.0)); + // a short track between ends of no value hardly strays + CHECK(track_uneven::along(20.0, 0.0, 0.0, 2.5, 5.0) < 1.0); + // a switch of more than the track: the track starts at the value of the switch + CHECK(track_uneven::along(18.0, 35.0, 18.0, 0.0, length) == doctest::Approx(35.0)); + CHECK(track_uneven::along(18.0, 35.0, 18.0, track_uneven::fade, length) == doctest::Approx(18.0)); + // the short piece inside a slip goes with its switches + 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 (int step = 1; step <= 2000; ++step) + { + 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; + } +} + +TEST_CASE("uneven track: the foot of the ballast stays where it was") +{ + double const rail{0.18}; + double const height{0.38}; + CHECK(track_uneven::bed_share(0.0, rail, height) == doctest::Approx(1.0)); // rail top + CHECK(track_uneven::bed_share(rail, rail, height) == doctest::Approx(1.0)); // top of the ballast + CHECK(track_uneven::bed_share(rail + 0.5 * height, rail, height) == doctest::Approx(0.5)); + CHECK(track_uneven::bed_share(rail + height, rail, height) == doctest::Approx(0.0)); // its foot + CHECK(track_uneven::bed_share(rail + 2.0 * height, rail, height) == doctest::Approx(0.0)); + CHECK(track_uneven::bed_share(rail + 1.0, rail, 0.0) == doctest::Approx(1.0)); // no ballast +} + +TEST_CASE("uneven track: the rails dip around a joint") +{ + CHECK(track_uneven::dip(0.0) == doctest::Approx(1.0)); + CHECK(track_uneven::dip(1.0) == doctest::Approx(0.75).epsilon(0.01)); // the worst case for rings 2 m apart + CHECK(track_uneven::dip(track_uneven::dip_reach) == doctest::Approx(0.0)); + CHECK(track_uneven::rail_phase(26.0, 25.0) == doctest::Approx(1.0)); + CHECK(track_uneven::rail_phase(-1.0, 25.0) == doctest::Approx(24.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/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/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/tests/unit/vehicle.cpp b/tests/unit/vehicle.cpp index 135c5c2fb..e38999f7d 100644 --- a/tests/unit/vehicle.cpp +++ b/tests/unit/vehicle.cpp @@ -1,57 +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"}}, -}; - -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) { @@ -64,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; @@ -76,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; @@ -140,48 +122,254 @@ 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) { + 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); + } + forces(Dt); + move(Track, Dt, location, m_mover->PipePress); +} + +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; - TLocation const location{0.0, 0.0, 0.0}; - TRotation const rotation{0.0, 0.0, 0.0}; + // 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); - m_mover->ComputeMovement(Dt, Dt, shape, param, traction, location, rotation); +} + +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}; + + // 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 + // 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); + } +} + +// 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->begin_step(); + placed.owner->forces(Dt); + } + for (auto &placed : m_vehicles) { + auto const &mover{placed.owner->mover()}; + placed.position += placed.heading * placed.owner->move(Track, Dt, TLocation{placed.position, 0.0, 0.0}, mover.PipePress); + } +} + +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; +} + +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 ad036839e..e14f4e464 100644 --- a/tests/unit/vehicle.h +++ b/tests/unit/vehicle.h @@ -1,8 +1,11 @@ #pragma once +#include #include +#include #include #include +#include #include @@ -10,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: @@ -19,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(); } @@ -54,18 +86,77 @@ 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: 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 +// 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/tools/track_uneven_grade.py b/tools/track_uneven_grade.py new file mode 100644 index 000000000..81df2ce2a --- /dev/null +++ b/tools/track_uneven_grade.py @@ -0,0 +1,665 @@ +#!/usr/bin/env python3 +"""Grades the tracks of any scenery for the uneven attribute, from what the scenery shows: textures, grass on the track, +curves, speed and road crossings. The originals stay as they are, graded copies of the files with tracks go to --out. + + python track_uneven_grade.py scenery/td.scn + python track_uneven_grade.py scenery/baltyk_skm1.scn --out graded + python track_uneven_grade.py scenery/baltyk_skm1.scn --out graded --route -107,2300 -4120,2491 --straight tor0290,tor0906 + python track_uneven_grade.py scenery/mytracks.scm --root scenery --out graded --gain 1.2 + +With no --out it only reports. See track_uneven_doc.md for the attribute. Python 3.8 or later, no other packages. +""" +import argparse +import collections +import heapq +import math +import pathlib +import re +import sys +import zlib + +# PLK Id-14 appendix 1, class C: vertical limit in mm by speed in km/h +LIMITS = [(20, 50), (30, 40), (40, 35), (50, 30), (60, 25), (70, 21), (80, 18), (90, 16), (100, 14), (110, 12), (120, 10), (130, 9), (140, 8), (150, 7), (160, 6)] +# how worn a texture looks, 0-1: the first pattern found in its name counts, anything else is 0.5 +BALLAST_WEAR = [("grass|traw", 0.95), ("old|star", 0.9), ("oil|smar", 0.6), ("new|nowa", 0.0), ("bps|beton|conc|tpb", 0.2), ("drew|wood|tpd", 0.45)] +RAIL_WEAR = [("unused2", 1.0), ("unused", 0.85), ("rused2", 0.8), ("rused", 0.65), ("used2", 0.45), ("used", 0.2), ("new", 0.0), ("old|rust", 0.8)] +WOOD = re.compile("drew|wood|tpd") +GRASS = re.compile("grass|traw|chwast|weed") +NO_SPEED = 40.0 # km/h, a stretch with no velocity anywhere: station and side tracks +SWITCH_MOST = 35.0 # the limit of 40 km/h +DIVERGING = 40.0 # km/h through the diverging path +MOST, MOST_UP = 50.0, 60.0 # of uneven and of its vertical part +REACH = 0.3 # how near the ends of tracks count as joined, m +SHORT = 2.0 # the pieces inside a slip don't tell the quality of the line +PIECE = 100.0 # a track without a speed shorter than this can take it through a switch, m +GRASS_NEAR, GRASS_FULL = 2.5, 14.0 # grass this near the middle grows on the track; plants per 100 m of an overgrown one +NEAR_LEAST, NEAR_MOST = 0.5, 0.95 # how far a chosen track goes towards the limit +CURVE_STEP = 1.0 / 2500.0 # a difference of curvature that counts as a change, 1/m +RISE = 0.0273 * 4.5 # how fast pressure lifts a pantograph, m/s, by TDynamicObject::Update +GAP_MOST = 0.006 # the gap to the wire allowed; at 0.01 m the voltage goes and the main breaker trips +OVERSPEED = 1.1 # a driver can go this much over the speed of the track +# as up_waves and the dip of the joints in world/TrackUneven.h +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*)?|\.\d+)([eE][-+]?\d+)?$") + + +def limit(speed): + if speed <= LIMITS[0][0]: + return float(LIMITS[0][1]) + for (v0, n0), (v1, n1) in zip(LIMITS, LIMITS[1:]): + if speed <= v1: + return n0 + (n1 - n0) * (speed - v0) / (v1 - v0) + return float(LIMITS[-1][1]) + + +def wear(name, table): + name = (name or "").lower() + for pattern, value in table: + if re.search(pattern, name): + return value + return 0.5 + + +def hashed(salt, name): + return zlib.crc32(salt + name.encode("latin-1", "replace")) / 0xffffffff + + +class Scenery: + """All that's found by following the includes from the given files.""" + + def __init__(self, root): + self.root = root + self.tracks, self.roads, self.plants = [], [], collections.defaultdict(list) + self.seen = set() + self.notes = collections.Counter() + self.texts = {} + + def read(self, path, offset=(0.0, 0.0, 0.0)): + path = path.resolve() + 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("\\", "/") + # a plant takes its place in the parameters; a file with grass in its name and none of them just lists plants + 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]) + elif not path.is_file(): + self.notes["brak pliku " + name] += 1 + 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 + self.notes["pliki z parametrami, tory pominiete"] += 1 + + def plant(self, x, z): + self.plants[(round(x / 10), round(z / 10))].append((x, z)) + + +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": + end += 1 + if end >= len(words) or at + 10 >= end: + return i + kind = lower[at + 1] + if kind not in ("normal", "switch", "road"): + return end + 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)] + b = [part["p2"][i] + part["c2"][i] for i in range(3)] + u = 1 - t + return [u * u * u * part["p1"][i] + 3 * u * u * t * a[i] + 3 * u * t * t * b[i] + t * t * t * part["p2"][i] for i in range(3)] + + +def line_of(part, step=5.0): + if not any(part["c1"]) and not any(part["c2"]): + return [list(part["p1"]), list(part["p2"])] + count = max(4, int(math.dist(part["p1"], part["p2"]) / step)) + return [bezier(part, i / count) for i in range(count + 1)] + + +def plan_radius(line): + least = math.inf + for a, b, c in zip(line, line[1:], line[2:]): + area = abs((b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2])) / 2 + if area > 1e-6: + least = min(least, math.dist((a[0], a[2]), (b[0], b[2])) * math.dist((b[0], b[2]), (c[0], c[2])) * math.dist((c[0], c[2]), (a[0], a[2])) / (4 * area)) + return least + + +def end_curve(part, key): + line = part["line"] + if len(line) < 3: + return 1.0 / part["radius"] if part["radius"] > 0 else 0.0 + a, b, c = line[:3] if key == "p1" else line[-3:] + sides = math.dist(a, b) * math.dist(b, c) * math.dist(c, a) + return 2.0 * ((b[0] - a[0]) * (c[2] - b[2]) - (b[2] - a[2]) * (c[0] - b[0])) / sides if sides > 0 else 0.0 + + +class Joints: + def __init__(self, tracks): + self.ends = collections.defaultdict(list) + for t in tracks: + for part in t["paths"]: + for key in ("p1", "p2"): + self.ends[(round(part[key][0]), round(part[key][2]))].append((t, part, key)) + + def at(self, point): + found = [] + for cx in (round(point[0]) - 1, round(point[0]), round(point[0]) + 1): + for cz in (round(point[2]) - 1, round(point[2]), round(point[2]) + 1): + for item in self.ends.get((cx, cz), ()): + if abs(item[1][item[2]][0] - point[0]) <= REACH and abs(item[1][item[2]][2] - point[2]) <= REACH: + found.append(item) + 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 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: + plain, further = joined_at(t, reached, joints, seen) + found += plain + onward += further + 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 + 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 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): + continue + for _, part, key in joints.at(point): + other = part["p2" if key == "p1" else "p1"] + onward = (round(other[0], 1), round(other[2], 1)) + if distance + part["length"] < best.get(onward, math.inf): + best[onward] = distance + part["length"] + heapq.heappush(heap, (best[onward], onward, other)) + return best + + +def grass_on(part, plants): + near, count = set(), max(1, int(part["length"] / 3)) + line = part["line"] + for i in range(count + 1): + position = i / count * (len(line) - 1) + low = min(int(position), len(line) - 2) + share = position - low + x, z = line[low][0] + (line[low + 1][0] - line[low][0]) * share, line[low][2] + (line[low + 1][2] - line[low][2]) * share + for cx in (round(x / 10) - 1, round(x / 10), round(x / 10) + 1): + for cz in (round(z / 10) - 1, round(z / 10), round(z / 10) + 1): + for plant in plants.get((cx, cz), ()): + if (plant[0] - x) ** 2 + (plant[1] - z) ** 2 < GRASS_NEAR * GRASS_NEAR: + near.add(plant) + 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 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): + 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): + # the height of the rail under a pantograph frame by frame, per mm of the vertical part + step = t["speed"] * OVERSPEED / 3.6 / 60.0 + heights, done = [], 0.0 + line = t["paths"][0]["line"] + for a, b in zip(line, line[1:]): + length = math.dist(a, b) + count = max(1, int(length / step)) + for i in range(count): + share = i / count + x, z = a[0] + (b[0] - a[0]) * share, a[2] + (b[2] - a[2]) * share + height = STRAY_PER_MM * sum(s * math.sin(wx * x + wz * z + phase) for wx, wz, phase, s in UP_WAVES) + if t["sound"] > 2.0 * DIP_REACH: + phase = math.fmod(done + length * share, t["sound"]) + joint = min(phase, t["sound"] - phase) + if joint < DIP_REACH: + height -= 0.001 * DIP_SHARE * math.cos(math.pi / 2 * joint / DIP_REACH) ** 2 + heights.append(height) + done += length + return heights + + +def largest_gap(heights, up): + gap = worst = 0.0 + climb = RISE / 60.0 + for before, after in zip(heights, heights[1:]): + gap = max(0.0, gap + (before - after) * up - climb) + worst = max(worst, gap) + return worst + + +def ceiling(t, product): + # the worst track of a speed comes up to it and the better ones stay under it, so the state of a track still shows + return product / t["speed"] * (0.7 + 0.3 * min(1.0, max(0.0, (t["state"] - 0.5) / 0.7))) + + +def path_radius(part): + return min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + + +def mark_diverging_paths(switches): + # the path that bends more + for t in switches: + radii = [path_radius(p) for p in t["paths"]] + t["bent"] = 1 if len(radii) < 2 or radii[1] <= radii[0] else 0 + + +class Route: + """How far along from the first point to the second a place lies, measured along the tracks.""" + + def __init__(self, tracks, joints, first, last): + self.first, self.last = first, last + self.from_first, self.from_last = distances(tracks, joints, first), distances(tracks, joints, last) + ends = [self.from_last[n] for n in self.from_last if math.dist(n, first) < 300] + self.length = min(ends) if ends else math.dist(first, last) + if not ends: + print("uwaga: po torach nie ma polaczenia miedzy punktami --route, licze w linii prostej") + + def share(self, part): + shares = [] + for key in ("p1", "p2"): + place = (part[key][0], part[key][2]) + node = (round(place[0], 1), round(place[1], 1)) + a, b = self.from_first.get(node), self.from_last.get(node) + if a is None or b is None: + a, b = math.dist(place, self.first), math.dist(place, self.last) + shares.append(min(1.0, max(0.0, (a - b + self.length) / (2 * self.length)))) + return 0.5 * sum(shares) + + +def grade_states(scenery, route, gain): + cells = road_cells(scenery.roads) + for t in scenery.tracks: + part = t["paths"][0] + radius = path_radius(part) + curve = 0.0 if radius >= 6000 else min(1.0, max(0.0, (1200 - radius) / 950)) + plain = t["kind"] == "normal" + density = grass_on(part, scenery.plants) if plain else 0.0 + crossing = plain and hits_road(part, cells) + progress = route.share(part) if route else 0.5 + state = 0.50 + 0.20 * wear(t["ballast"], BALLAST_WEAR) + 0.10 * wear(t["rail"], RAIL_WEAR) + 0.12 * min(1.0, density / GRASS_FULL) + 0.06 * curve + 0.22 * progress + 0.18 * crossing + 0.06 * (hashed(b"", t["name"]) - 0.5) + t.update(state=min(state, 1.2) * gain, curve=curve, crossing=crossing, grass=density) + if plain: + value = min(limit(t["speed"]) * t["state"], MOST) + t.update(value=value, up=min(value * 1.25, MOST_UP), side=min(value * (0.9 + 0.4 * curve), MOST)) + + +def fit_under_pantograph(normal): + # the largest product of the vertical part and the speed with the gap small on every track + low, high = 100, 4000 + while high - low > 25: + middle = (low + high) // 2 + if all(largest_gap(t["heights"], min(t["up"], ceiling(t, middle))) <= GAP_MOST for t in normal): + low = middle + else: + high = middle + for t in normal: + up = ceiling(t, low) + if up < t["up"]: + ratio = up / t["up"] + t["up"], t["value"], t["side"] = up, t["value"] * ratio, t["side"] * ratio + 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] + 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): + # on wooden sleepers and where the curve changes, a share of the tracks goes nearer the limit, chosen for good by the place + chosen = 0 + for t in normal: + if not (WOOD.search(t["ballast"] or "") or curve_changes(t, joints)): + continue + part = t["paths"][0] + name = f"{t['name']}:{part['p1'][0]:.0f}:{part['p1'][2]:.0f}" + if hashed(b"near", name) >= share: + continue + value = t["value"] + (limit(t["speed"]) - t["value"]) * (NEAR_LEAST + (NEAR_MOST - NEAR_LEAST) * hashed(b"how", name)) + if value <= t["value"]: + continue + # sideways and the cross level don't move a pantograph, up and down only as far as it keeps to the wire on this track + up = value + while up > t["up"] and largest_gap(t["heights"], up) > GAP_MOST: + up -= 0.5 + t["value"], t["up"], t["side"] = value, max(up, t["up"]), min(value * (0.9 + 0.4 * t["curve"]), MOST) + chosen += 1 + 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"] = [value_from_plain(t, index, part, joints, product) for index, part in enumerate(t["paths"])] + for _ in range(5): + for t in switches: + 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"]] + + +def grade(scenery, options): + tracks = scenery.tracks + normal = [t for t in tracks if t["kind"] == "normal"] + switches = [t for t in tracks if t["kind"] == "switch"] + joints = Joints(tracks) + mark_diverging_paths(switches) + settle_speeds(tracks, joints) + grade_states(scenery, Route(tracks, joints, *options.route) if options.route else None, options.gain) + for t in normal: + t["heights"] = heights_per_mm(t) + product = fit_under_pantograph(normal) + chosen = move_near_limit(normal, joints, options.near) + worst = max((largest_gap(t["heights"], t["up"]) for t in normal), default=0.0) + for t in normal: + del t["heights"] + if t["name"].lower() in options.straight: + t["value"] = t["up"] = t["side"] = 0.0 + grade_switches(switches, joints, product) + return product, chosen, worst + + +def report(scenery, product, chosen, worst): + normal = [t for t in scenery.tracks if t["kind"] == "normal"] + switches = [t for t in scenery.tracks if t["kind"] == "switch"] + print(f"pliki: {len(scenery.seen)}, tory: {len(normal)}, rozjazdy: {len(switches)}, drogi: {len(scenery.roads)}, trawa: {sum(len(v) for v in scenery.plants.values())}") + print(f"pion razy km/h najwyzej {product}, blizej granicy {chosen} torow, najwieksza przerwa do drutu {worst * 100:.2f} cm (bez wznosu samego drutu)") + groups = collections.defaultdict(list) + for t in normal: + groups[(t["source"], t["speed"])].append(t) + print("predkosc skad torow km limit uneven min/mediana/max pion max") + for key in sorted(groups, key=lambda k: (-k[1], k[0])): + rows = groups[key] + values = sorted(t["value"] for t in rows) + print(f"{key[1]:8.0f} {key[0]:8} {len(rows):6} {sum(t['paths'][0]['length'] for t in rows) / 1000:6.1f} {limit(key[1]):6.1f} {values[0]:6.1f} {values[len(values) // 2]:6.1f} {values[-1]:6.1f} {max(t['up'] for t in rows):8.1f}") + if switches: + values = sorted(v for t in switches for v in t["values"]) + print(f"rozjazdy: {values[0]:.1f}-{values[-1]:.1f} mm, mediana {values[len(values) // 2]:.1f}") + for note, count in sorted(scenery.notes.items()): + print(f"uwaga: {note}: {count}" if not note.startswith("brak") else f"uwaga: {note}") + if scenery.notes["rotate"]: + print("uwaga: bloki rotate nie sa uwzgledniane, polaczenia torow z nich moga byc nierozpoznane") + taken = sum(1 for t in scenery.tracks if t["taken"]) + if taken: + print(f"uwaga: {taken} torow ma juz nierownosci, zostaja bez zmian") + + +def attribute(t): + if t["kind"] == "normal": + return f"uneven {t['value']:.1f};{t['up']:.1f};{t['side']:.1f} " if t["value"] > 0 else "" + return "uneven " + ";".join(f"{v:.1f}" for v in t["values"]) + " " + + +def write(scenery, out): + by_file = collections.defaultdict(list) + for t in scenery.tracks: + if not t["taken"]: + by_file[t["file"]].append(t) + written = 0 + for path, tracks in by_file.items(): + text = scenery.texts[path] + pieces, last = [], 0 + for t in sorted(tracks, key=lambda t: t["offset"]): + pieces += [text[last:t["offset"]], attribute(t)] + last = t["offset"] + pieces.append(text[last:]) + target = out / path.relative_to(scenery.root) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes("".join(pieces).encode("latin-1")) + written += 1 + print(f"zapisane pliki: {written}, w {out}") + + +def point(text): + x, z = text.split(",") + return float(x), float(z) + + +def main(): + parser = argparse.ArgumentParser(description="Ocena torow dowolnej scenerii dla atrybutu uneven.") + parser.add_argument("files", nargs="+", type=pathlib.Path, help=".scn, .scm albo .inc; includy sa czytane dalej") + parser.add_argument("--root", type=pathlib.Path, help="folder scenery, wzgledem ktorego ida includy; domyslnie folder pierwszego pliku") + parser.add_argument("--out", type=pathlib.Path, help="folder na kopie plikow z torami; bez tego tylko raport") + parser.add_argument("--route", nargs=2, type=point, metavar="X,Z", help="tory pogarszaja sie od pierwszego punktu do drugiego, liczac po torach") + parser.add_argument("--gain", type=float, default=1.0, help="mnoznik stanu torow, domyslnie 1") + parser.add_argument("--near", type=float, default=0.6, help="jaka czesc torow na drewnie i przy zmianach krzywizny idzie blizej granicy Id-14, domyslnie 0.6") + parser.add_argument("--straight", default="", help="nazwy torow bez nierownosci, po przecinku") + options = parser.parse_args() + options.straight = {name.strip().lower() for name in options.straight.split(",") if name.strip()} + root =(options.root or options.files[0].parent).resolve() + if options.out: + options.out = options.out.resolve() + if options.out == root or root in options.out.parents: + sys.exit("--out nie moze lezec w folderze scenerii: oryginaly maja zostac nietkniete") + scenery = Scenery(root) + for path in options.files: + scenery.read(path) + if not scenery.tracks: + sys.exit("nie znaleziono torow") + product, chosen, worst = grade(scenery, options) + report(scenery, product, chosen, worst) + if options.out: + write(scenery, options.out) + + +if __name__ == "__main__": + main() diff --git a/track_uneven_doc.md b/track_uneven_doc.md new file mode 100644 index 000000000..e4822b971 --- /dev/null +++ b/track_uneven_doc.md @@ -0,0 +1,119 @@ +# Track unevenness + +An optional attribute of a track entry, before `endtrack`: + +``` +node -1 0 tor1 track normal 100.0 1.435 0.15 25.0 20 0 flat vis +rail_screw_used1 6 1435mm/tpbps-new2 0.2 0.5 1.1 +0.0 0.2 0.0 0.0 +0.0 0.0 0.0 +0.0 0.0 0.0 +0.0 0.2 100.0 0.0 +0.0 +uneven 18;;;25;6 +endtrack +``` + +The numbers are in mm, 0-100, separated by semicolons with no spaces: + +| position | `normal` track | `switch` | +|----------|-----------------------------------------------|--------------------------| +| 1 | peak deviation of a rail on a 10 m chord | the same, path p1-p2 | +| 2 | vertical | path p3-p4 | +| 3 | sideways | path p1-p2, vertical | +| 4 | cross level | path p1-p2, sideways | +| 5 | dip at rail joints, by default 0.4 of vertical | path p3-p4, vertical | +| 6 | | path p3-p4, sideways | + +A position left out takes the first number, and the dip follows the vertical value. Positions at the end can be dropped. Sideways alone is `uneven 0;;30`, and dips at the joints alone are `uneven 0;;;;6`. A switch with no components of its own on path p3-p4 takes the values of path p1-p2. A switch for 100 km/h on the straight path and 40 km/h on the diverging one is, for example, `uneven 14;35`. + +All of them are also in the editor, in the track panel, section "Ride and sound". On roads, rivers and turntables they are ignored. + +## Choosing a value + +`uneven` corresponds to the "vertical irregularities" column of the PLK Id-14 instruction, appendix 1 (class C): + +| speed [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | +|---------------|----|----|----|----|-----|-----|-----| +| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | + +According to the instruction, a track in good condition is half of this value (class A), and one in fair condition 3/4 (class B). + +At these limits a pantograph may not keep up at speed, which trips the main breaker: see the end of "How it works". `tools/track_uneven_grade.py` keeps the values below that. + +A track with `uneven 18` measures as follows (`tests/unit/test_track_uneven.cpp`), against the Id-14 limit for 80 km/h: + +| parameter | result | Id-14 | +|--------------------------|--------|-----------| +| vertical irregularities | 18 mm | 18 mm | +| horizontal irregularities | 19 mm | 17 mm | +| twist on a 5 m base | 15 mm | 16 mm | +| gauge | ±10 mm | +10/-8 mm | +| cross level | 20 mm | 20 mm | +| J index | 6.1 | 5.3 | + +## How it works + +Unevenness is a displacement field that depends on the position on the map, made of waves 5-42 m long. Vertically there are fewer short waves, so the rails come near the peak value more often, and there are also waves of 47-83 m: they hardly count on the 10 m chord, the body rises and falls on them, and a pantograph keeps up with them. Nothing in it is random: one place bends the same way on every run. A track that is moved gets the waves of its new place. + +Each component joins separately: + +- **Track to track:** the lower of the two values applies at the joint, and each track comes to it smoothly over 20 m. A free end of a track stays as designed. +- **Switch:** each path has its own value, and the lower of the two applies at their common end. Directly joined switches take the lowest value of the same path, and where two paths meet, the lower of the two. A joined track comes to the value of the switch at the end it meets, even if that is higher than its own. A track with none of a component keeps the whole switch as designed in it. +- **Joints:** the dips come every `fSoundDistance`, over 6 m, and run on through following tracks with the same spacing. The wheel clatter falls where the dip is. With a spacing of 6 m or less, and on switches, there are no dips. +- **Cross level:** adds to the designed cant. It is a tilt of the map plan, so tracks laid towards each other meet at the same cant. On switches there is only the part coming from the waves, with no separate settling of the rails. + +The ends of joined paths meet in the middle between them, with one vector across, if they are less than 20 cm apart. This closes the cracks from coordinates that don't match in the data, on tracks with no unevenness as well. At such a joint a switch takes the cant of the plain track, and two tracks or two switches meet halfway. This applies to the physics too. + +The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the body and the cab shake follow the unevenness. The slope of the ballast takes up the difference, and its foot stays as designed, on the ground. + +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, 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 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 | +| 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. + +The physics takes the curve radius and the grade from the design, so unevenness doesn't change them. It adds only the cross level towards derailment, about 0.06 m/s² at `uneven 18` against a threshold of about 3.2 m/s². + +## Grading the tracks of any scenery + +`tools/track_uneven_grade.py` works out the values from what the scenery shows: ballast and rail textures, grass on the track, curves, speed and road crossings. It reads a `.scn`, `.scm` or `.inc` file with its includes and leaves the originals as they are, writing copies of the files with tracks to `--out`. With no `--out` it only prints a report. + +``` +python tools/track_uneven_grade.py scenery/lk357.scn --out graded +python tools/track_uneven_grade.py scenery/baltyk_skm1.scn --out graded --route -107,2300 -4120,2491 +``` + +- `--route X,Z X,Z`: tracks get worse from the first point to the second, measured along the tracks. +- `--gain`: multiplier of the state of the tracks, 1 by default. +- `--near`: the share of tracks on wooden sleepers and at changes of curve that go nearer the Id-14 limit, 0.6 by default. The choice is fixed. +- `--straight`: names of tracks with no unevenness, separated by commas. +- `--root`: the `scenery` folder, when the given file lies deeper. + +The values stay under what a pantograph can follow, by a simulation over those tracks, but the rise of the wire itself isn't counted. Texture wear is read from the names, by the tables at the top of the script, and an unknown name gets 0.5. Tracks in `.inc` files taking parameters are skipped and `rotate` blocks aren't applied; the report says so in both cases. A track that already has unevenness stays as it is. + +## Performance + +A track with unevenness has geometry every 2 m instead of every 10 m on straights. Switches keep their division. + +Measured (camera moving or in the cab, 1920×1080): + +| scene | no flags | flags 18/35 | +|-------------------------------------|-----------------------|------------------------| +| Bałtyk, 56 km of tracks: vertices | 0.56 M (about 27 MB) | 2.17 M (about 104 MB) | +| Bałtyk: longest frame in a fly-through | 31.5 ms | 64 ms | +| `td.scn`, driving at 120 km/h: frame | 2.73 ms | 2.93 ms | + +The cost grows with the number of flagged tracks, and the longest frames occur when the geometry of further sections is built. Driving on such a track costs practically nothing: about 0.3 µs per axle in a physics step. diff --git a/utilities/Logs.cpp b/utilities/Logs.cpp index 38f8062b9..38094d72e 100644 --- a/utilities/Logs.cpp +++ b/utilities/Logs.cpp @@ -73,12 +73,28 @@ std::deque ErrorStack; // lock for log stacks std::mutex logMutex; +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() +{ + log_stopped() = 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 = log_stopped(); { // --- 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 ); 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/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/utilities/utilities.cpp b/utilities/utilities.cpp index 02608262b..e10cf0d84 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) @@ -471,6 +474,26 @@ bool FileExists(std::string const &Filename) return std::filesystem::exists(Filename); } +std::string FileCased(std::filesystem::path const &Filename) +{ + std::error_code error; + if (std::filesystem::exists(Filename, error)) + 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 + 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 Filename.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 5392ba6c5..cc4056cae 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. 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::filesystem::path const &Filename); std::pair FileExists(std::vector const &Names, std::vector const &Extensions); diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index ea78e655c..9f83b6d53 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..d5f60e332 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; } @@ -3627,6 +3619,7 @@ bool TDynamicObject::Update(double dt, double dt1) if( dRailLength > 0.0 ) { for( auto &axle : m_axlesounds ) { axle.distance = axle.offset; + axle.atjoints = false; } } dRailLength = MyTrack->fSoundDistance; @@ -3660,36 +3653,32 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - axle.distance += dDOMoveLen * directioninconsist; - if( axle.distance < 0 - || axle.distance > dRailLength ) { - axle.distance = clamp_circular( axle.distance, dRailLength ); - 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; } @@ -4150,6 +4139,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 +4171,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 +4818,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 ); @@ -4858,7 +4852,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 @@ -6363,6 +6357,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 +6583,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 +7822,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 +7873,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 +7893,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() ) { @@ -8029,17 +8081,25 @@ 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, 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( - -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 + auto const ride { 0.5 * ( Axle0.UnevenBend() + Axle1.UnevenBend() ) * ( MoverParameters->V * MoverParameters->V ) }; + shakevector += glm::dvec3( + -glm::dot( ride, vLeft ) * Timedelta * 5.0 * Global.ShakingMultiplierRL, + -ride.y * Timedelta * Global.ShakingMultiplierUD, + 0.0 ); } auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; - if( LocalRandom( iVel ) > 25.0 ) { + if( false == replaced.jolts && LocalRandom( iVel ) > 25.0 ) { // extra shake at increased velocity shake += ShakeSpring.ComputateForces( glm::dvec3( @@ -8065,6 +8125,28 @@ TDynamicObject::update_shake( double const Timedelta ) { } } +bool +TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const { + + // 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 * follower.GetTrack()->fSoundDistance }; + Axle.distance = railphase; + Axle.atjoints = true; + return passed; + } + Axle.atjoints = false; + Axle.distance += Move * Direction; + if( Axle.distance >= 0 && Axle.distance <= dRailLength ) { + return false; + } + Axle.distance = clamp_circular( Axle.distance, dRailLength ); + return true; +} + std::pair TDynamicObject::shake_angles() const { diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 36335a320..577f0f020 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 }; @@ -402,7 +410,10 @@ private: double distance; // distance to rail joint double offset; // axle offset from centre of the vehicle sound_source clatter; // clatter emitter + 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 ) const; struct powertrain_sounds { sound_source inverter { sound_placement::engine }; @@ -567,6 +578,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 +716,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 +830,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/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 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 } }, diff --git a/world/Segment.cpp b/world/Segment.cpp index 69886f42c..1aff4ae42 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -69,6 +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_setting.designrolls = { fRoll1, fRoll2 }; + m_setting.joints = {}; bCurve = bIsCurve; if (Global.bRollFix) { // Ra: poprawianie przechyłki @@ -118,6 +120,10 @@ 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_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 ); + } // NOTE: optionally replace this part with the commented version, after solving geometry issues with double switches if( pOwner->eType == tt_Switch && fStep * (3.0 * Global.SplineFidelity) > fLength ) { @@ -365,6 +371,48 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const }; */ +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 }; + result *= track_uneven::stray_per_mm; + result.y -= pOwner->joint_dip( Distance ); + return result; +} + +glm::dvec3 TSegment::uneven_bend( double const Distance ) const { + if( false == m_setting.uneven ) { + return glm::dvec3 { 0.0 }; + } + // over about a bogie + auto const reach { 1.5 }; + 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 ); +} + +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_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 + auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; + auto const across { std::clamp( glm::dot( Vertex.position - Position, Parallel ), -0.5f * gauge, 0.5f * gauge ) }; + auto const share { pOwner->uneven_bed_share( Position.y - Vertex.position.y ) }; + if( share <= 0.0 ) { + return; + } + 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::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 @@ -388,8 +436,31 @@ void TSegment::RaPositionGet(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 { + if( fTsBuffer.empty() ) { + // never made into a path: tt_Cross and the like + Point = Point1; + Across = glm::dvec3 { 0.0 }; + return; + } + auto const t { FastGetTFromS( std::clamp( Distance, 0.0, fLength ) ) }; + Point = FastGetPoint( t ); + Across = track_uneven::across_of( FastGetDirection( t, 0.1 / fLength ), FastGetPoint_1() - FastGetPoint_0() ); +} + glm::dvec3 TSegment::FastGetPoint(double const t) const { // return (bCurve?std::lerp(t,Point1,CPointOut,CPointIn,Point2):((1.0-t)*Point1+(t)*Point2)); @@ -441,6 +512,10 @@ 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_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; fEnd = static_cast(fLength * double( iEnd ) / double( iSegCount )); @@ -486,6 +561,10 @@ 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_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 if( Transition ) { @@ -500,6 +579,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ( jmm1 * ShapePoints[ j ].texture.x + m1 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv1 } ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); } // jeśli jest wskaźnik do tablicy if (p && *p && !j) // to dla pierwszego punktu @@ -518,6 +598,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ( jmm2 * ShapePoints[ j ].texture.x + m2 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv2 } ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } // jeśli jest wskaźnik do tablicy // to dla pierwszego punktu @@ -540,6 +621,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv1 } ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); pt = parallel2 * ( jmm2 * ( ShapePoints[ j ].position.x - fOffsetX.first ) + m2 * ( ShapePoints[ j ].position.x - fOffsetX.second ) ) + pos2; pt.y += ShapePoints[ j ].position.y; @@ -550,6 +632,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv2 } ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } } } diff --git a/world/Segment.h b/world/Segment.h index a3c83ade7..05522e602 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -9,8 +9,13 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include +#include + #include "utilities/Classes.h" #include "rendering/geometrybank.h" +#include "world/TrackUneven.h" #include struct map_colored_paths { @@ -47,6 +52,15 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach + // 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; + }; + 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 @@ -67,6 +81,11 @@ class TSegment glm::dvec3 RaInterpolate(double const t) const; glm::dvec3 RaInterpolate0(double const t) const; + // moves a lofted vertex with the unevenness of the track + 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; + public: bool bCurve = false; @@ -106,6 +125,24 @@ public: /// Calculated position /// Calculated rotation void RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const; + // call before Init, it sets the density of the geometry + void + set_uneven( bool const Uneven, int const Path = 0 ) { + 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_setting.designrolls[ End ]; } + void + set_joint( int const End, std::optional> const &Joint, float const Roll ) { + 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 + uneven_bend( double const Distance ) const; glm::dvec3 FastGetPoint(double const t) const; inline glm::dvec3 diff --git a/world/Track.cpp b/world/Track.cpp index f1a29798c..1361ebd95 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 @@ -47,6 +49,7 @@ TIsolated *TIsolated::pRoot = nullptr; TTrack::profiles_array TTrack::m_profiles; TTrack::profiles_map TTrack::m_profilesmap; +int TTrack::s_layoutstamp { 0 }; TSwitchExtension::TSwitchExtension(TTrack *owner, int const what) { // na początku wszystko puste @@ -325,6 +328,7 @@ void TTrack::ConnectPrevPrev(TTrack *pTrack, int typ) { //łączenie torów - Point1 własny do Point1 cudzego if (pTrack) { //(pTrack) może być zwrotnicą, a (this) tylko zwykłym odcinkiem + ++s_layoutstamp; trPrev = pTrack; iPrevDirection = pTrack->eType == tt_Switch ? 0 : typ & 2; pTrack->trPrev = this; @@ -335,6 +339,7 @@ void TTrack::ConnectPrevNext(TTrack *pTrack, int typ) { //łaczenie torów - Point1 własny do Point2 cudzego if (pTrack) { + ++s_layoutstamp; trPrev = pTrack; iPrevDirection = typ | 1; // 1:zwykły lub pierwszy zwrotnicy, 3:drugi zwrotnicy pTrack->trNext = this; @@ -354,6 +359,7 @@ void TTrack::ConnectNextPrev(TTrack *pTrack, int typ) { //łaczenie torów - Point2 własny do Point1 cudzego if (pTrack) { + ++s_layoutstamp; trNext = pTrack; iNextDirection = pTrack->eType == tt_Switch ? 0 : typ & 2; pTrack->trPrev = this; @@ -373,6 +379,7 @@ void TTrack::ConnectNextNext(TTrack *pTrack, int typ) { //łaczenie torów - Point2 własny do Point2 cudzego if (pTrack) { + ++s_layoutstamp; trNext = pTrack; iNextDirection = typ | 1; // 1:zwykły lub pierwszy zwrotnicy, 3:drugi zwrotnicy pTrack->trNext = this; @@ -427,11 +434,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(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; } else - eType = tt_Unknown; + eType = tt_Unknown; // left out by the loader if (Global.iWriteLogEnabled & 4) WriteLog(str); parser->getTokens(4); @@ -517,6 +528,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 ); @@ -633,6 +649,13 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_profile1 = fetch_track_rail_profile( railprofile ); } } + else if( str == "uneven" ) { + // uneven [;...]: see TrackUneven.h. The semicolons stay in the token + auto const values { parser->getToken( false, "\n\r\t " ) }; + if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { + m_uneven = track_uneven::read_attribute( values, eType == tt_Switch ); + } + } else if( str == "friction" ) { // memory cell holding friction value modifiers m_friction.first = parser->getToken(); @@ -674,6 +697,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) *parser >> token; str = token; } + // the segments were made before the attributes were read, and unevenness changes them + if( is_uneven() ) { + init_segments( false ); + } // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) { @@ -681,6 +708,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 @@ -705,6 +736,14 @@ void TTrack::init_segments( bool const Initial ) { fTexHeight1 -= fTexHeightOffset; fTexHeightOffset = 0.f; iTrapezoid = 0; + ++s_layoutstamp; + if( eType == tt_Switch && SwitchExtension ) { + SwitchExtension->Segments[ 0 ]->set_uneven( is_uneven() ); + SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven(), 1 ); + } + else if( Segment ) { + Segment->set_uneven( is_uneven() ); + } switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym @@ -1351,6 +1390,8 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const // wypełnianie tablic VBO void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { + m_drawnrun = {}; + Geometry1.clear(); Geometry2.clear(); if( SwitchExtension ) { @@ -1365,6 +1406,10 @@ 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() ) { @@ -2306,24 +2351,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 ? ( - 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() << ' ' @@ -2445,6 +2503,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } + Output << track_uneven::write_attribute( m_uneven, eType == tt_Switch ); if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } @@ -3632,6 +3691,11 @@ path_table::InitTracks() { isolated = isolated->Next(); } + // the physics goes by the cant at the joints too, and comes before any geometry is made + for( auto *track : m_items ) { + track->share_joints(); + } + TTrack::fetch_default_profiles(); } diff --git a/world/Track.h b/world/Track.h index 72a6b7ff4..ba8955032 100644 --- a/world/Track.h +++ b/world/Track.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "world/Segment.h" +#include "world/TrackUneven.h" #include "model/material.h" #include "scene/scenenode.h" #include "utilities/Names.h" @@ -187,6 +188,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; @@ -238,6 +240,7 @@ public: TEnvironmentType eEnvironment = e_flat; // dźwięk i oświetlenie int iAction = 0; // czy modyfikowany eventami (specjalna obsługa przy skanowaniu) float fOverhead = -1.0; // można normalnie pobierać prąd (0 dla jazdy bezprądowej po danym odcinku, >0-z opuszczonym i ograniczeniem prędkości) + track_uneven::attribute m_uneven; // plain tracks and switches private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) public: @@ -339,6 +342,29 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; + // a switch has two paths, any other track the one + int path_count() const; + TSegment *path_segment( int const Path ) const; + TTrack *path_neighbour( int const Path, int const End ) const; + // 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 + 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 ); // 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(); @@ -357,6 +383,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 @@ -370,6 +398,33 @@ private: void create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); void create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); void create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previous, TTrack const *Next ); + // rail joints run on across joined tracks, however the line is cut into them, so the clatter keeps its rhythm + struct rail_run { + double origin { 0.0 }; // position along the run of Point1 + double way { 1.0 }; // direction of the path along the run + int stamp { -1 }; + }; + 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 { + std::array paths; + std::array, 2> ends; + int stamp { -1 }; + }; + 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 *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. 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 new file mode 100644 index 000000000..10bf48d62 --- /dev/null +++ b/world/TrackUneven.cpp @@ -0,0 +1,386 @@ +/* +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 "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. + +namespace { + +double const switch_end_reach { 0.3 }; // how near an end of a path counts as being at a point, m +double const joint_reach { 0.2 }; // ends of joined paths further apart than this stay as they are, m +double const joint_alike { 0.9 }; // ends of joined paths less alike in direction than this stay as they are, cosine + +bool is_switch( TTrack const *Track ) { + + return Track != nullptr && Track->eType == tt_Switch && Track->SwitchExtension; +} + +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 &Switch ) { + + 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::span 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 && false == is_switch( other ) ) { + keep_to_plain_track( values, *other ); + } + } + } + return values; +} + +// the lowest Value( path, end ) of the ends of the paths at the point; Otherwise when none of them is there +template +track_uneven::amounts least_at_ends( TTrack const &Switch, glm::dvec3 const &Point, track_uneven::amounts const &Otherwise, Value_ const &Value ) { + + auto found { false }; + 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( end_point( *Switch.path_segment( path ), end ) - Point ) < switch_end_reach ) { + result = track_uneven::least( result, Value( path, end ) ); + found = true; + } + } + } + return found ? result : Otherwise; +} + +// how the end of a path meets the track joined to it +struct path_joint { + std::optional> place; // none: too far apart, or going different ways + float roll; +}; + +path_joint meet( TTrack const &Track, TSegment const &Segment, int const End, TTrack const &Neighbour ) { + + path_joint result { {}, Segment.design_roll( End ) }; + glm::dvec3 point; + glm::dvec3 across; + Segment.plan_frame( End ? Segment.GetLength() : 0.0, point, across ); + auto closest { joint_reach }; + for( int path = 0; path < Neighbour.path_count(); ++path ) { + auto const *theirs { Neighbour.path_segment( path ) }; + if( theirs == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + // laid towards each other, they look across and lean to opposite sides + auto const opposite { end == End }; + glm::dvec3 theirpoint; + glm::dvec3 theiracross; + theirs->plan_frame( end ? theirs->GetLength() : 0.0, theirpoint, theiracross ); + if( opposite ) { + theiracross = -theiracross; + } + auto const gap { glm::length( theirpoint - point ) }; + if( gap >= closest || glm::dot( across, theiracross ) < joint_alike ) { + continue; + } + closest = gap; + result.place.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); + if( Track.iCategoryFlag & 1 ) { + result.roll = track_uneven::joint_roll( + Segment.design_roll( End ), + opposite ? -theirs->design_roll( end ) : theirs->design_roll( end ), + Track.eType == tt_Switch, + Neighbour.eType == tt_Switch ); + } + } + } + return result; +} + +} // namespace + +int TTrack::path_count() const { + + return eType == tt_Switch ? 2 : 1; +} + +TSegment *TTrack::path_segment( int const Path ) const { + + return eType == tt_Switch ? SwitchExtension->Segments[ Path ].get() : Segment.get(); +} + +TTrack *TTrack::path_neighbour( int const Path, int const End ) const { + + if( eType == tt_Switch ) { + return End == 0 ? SwitchExtension->pPrevs[ Path ] : SwitchExtension->pNexts[ Path ]; + } + return End == 0 ? trPrev : trNext; +} + +track_uneven::amounts TTrack::uneven_declared( int const Path ) const { + + return track_uneven::declared( m_uneven, eType == tt_Normal, Path ); +} + +bool TTrack::is_uneven() const { + + auto const any = []( track_uneven::amounts const &Amount ) { + return Amount.up > 0.0 || Amount.side > 0.0 || Amount.cross > 0.0; }; + 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 ); +} + +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, 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 ) const { + + if( Other == nullptr ) { + return {}; + } + 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() { + + if( m_switchstray.stamp != s_layoutstamp ) { + settle_switch_stray(); + } + return m_switchstray; +} + +void TTrack::settle_switch_stray() { + + auto const group { switch_group( *this ) }; + auto const values { group_path_values( group ) }; + for( auto *track : group ) { + track->m_switchstray.paths = values; + track->m_switchstray.stamp = s_layoutstamp; + } + // 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; + } + } +} + +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 ] ), [ &paths ]( int const Path, int ) { return paths[ Path ]; } ); +} + +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 ] ), [ &stray ]( int const Path, int const End ) { return stray.ends[ Path ][ End ]; } ); +} + +double TTrack::joint_depth() const { + + if( eType != tt_Normal ) { + return 0.0; + } + return ( m_uneven.dip >= 0.f ? m_uneven.dip : track_uneven::dip_share * uneven_declared().up ); +} + +double TTrack::joint_dip( double const Distance ) { + + auto const phase { rail_phase( Distance ) }; + if( phase < 0.0 ) { + return 0.0; + } + auto const fromjoint { std::min( phase, fSoundDistance - phase ) }; + if( fromjoint >= track_uneven::dip_reach ) { + return 0.0; + } + auto const depth { joint_depth() }; + 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 ) { + + if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { + return -1.0; + } + if( m_railrun.stamp != s_layoutstamp ) { + settle_rail_run(); + } + 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 + 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 = [ this, &onward ]( int &End ) { + auto *track { this }; + int joined { 0 }; + for( int step = 0; step < 1000000; ++step ) { + auto *next { onward( *track, End, joined ) }; + if( next == nullptr || next == this ) { + break; + } + track = next; + End = 1 - joined; + } + return track; }; + auto const place = []( TTrack const &Track, int const 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 *track { walk( end ) }; + if( auto *other { walk( otherend ) }; place( *other, otherend ) < place( *track, end ) ) { + track = other; + end = otherend; + } + auto along { 0.0 }; + while( track != nullptr && track->m_railrun.stamp != s_layoutstamp ) { + auto const length { track->Length() }; + track->m_railrun = { end == 0 ? along : along + length, end == 0 ? 1.0 : -1.0, s_layoutstamp }; + along += length; + int joined { 0 }; + track = onward( *track, 1 - end, joined ); + end = joined; + } + // tracks joined one way only can leave this one out of the run: it stands on its own then, not to walk all of it again on every call + if( m_railrun.stamp != s_layoutstamp ) { + m_railrun = { 0.0, 1.0, s_layoutstamp }; + } +} + +void TTrack::share_joints() { + + for( int path = 0; path < path_count(); ++path ) { + auto *segment { path_segment( path ) }; + if( segment == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + auto const *neighbour { path_neighbour( path, end ) }; + auto const joined { neighbour != nullptr && neighbour != this && ( neighbour->iCategoryFlag & 15 ) == ( iCategoryFlag & 15 ) }; + auto const joint { joined ? meet( *this, *segment, end, *neighbour ) : path_joint { {}, segment->design_roll( end ) } }; + segment->set_joint( end, joint.place, joint.roll ); + } + } + if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { + 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 new file mode 100644 index 000000000..8e0f07750 --- /dev/null +++ b/world/TrackUneven.h @@ -0,0 +1,297 @@ +/* +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 +#include +#include +#include +#include +#include +#include +#include +#include + +// Track unevenness: a displacement field over the plan of the scenery, so everything at one place moves together: joined tracks, the +// paths of a switch, sleepers, axles. Waves 5-42 m long, shares after the German track spectrum with the long waves cut down; up and down to 83 m. +// The "uneven" value is the peak deviation of a rail on a 10 m chord, as in PLK Id-14 appendix 1; see test_track_uneven.cpp. +namespace track_uneven { + +inline constexpr float most { 100.f }; // largest value of the attributes, mm +inline constexpr double stray_per_mm { 0.00145 }; // field amplitude per mm of "uneven", m +inline constexpr double cross_share { 0.53 }; // share of the field amplitude the rails settle apart by +inline constexpr double fade { 20.0 }; // length over which a track goes from the value at its end to its own, m +inline constexpr double dip_share { 0.4 }; // joint dip depth per mm of "uneven", mm +inline constexpr double dip_reach { 3.0 }; // reach of the dip on each side of a joint, m + +// the value of each component of the unevenness, mm +struct amounts { + double up { 0.0 }; + double side { 0.0 }; + double cross { 0.0 }; +}; + +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; + double phase; + double share; +}; + +// the numbers of an entry like "uneven 14;35", negative where left out +template +std::array read_fields( std::string_view const Text ) { + + std::array result; + result.fill( -1.f ); + 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 ); + } + if( end == rest.size() ) { + break; + } + rest.remove_prefix( end + 1 ); + } + return result; +} + +// and back, with the ones left out at the end dropped +template +std::string write_fields( std::array const &Fields ) { + + auto count { Count }; + while( count > 1 && Fields[ count - 1 ] < 0.f ) { + --count; + } + std::ostringstream text; + for( std::size_t index = 0; index < count; ++index ) { + if( index > 0 ) { + text << ';'; + } + if( Fields[ index ] >= 0.f ) { + text << Fields[ index ]; + } + } + return text.str(); +} + +// the values of the attribute, mm, negative where not given. Cross level and the dip at joints for plain tracks, the path p3-p4 for switches +struct attribute { + float value { 0.f }; + float up { -1.f }; + float side { -1.f }; + float cross { -1.f }; + float dip { -1.f }; + float branch { -1.f }; + float branchup { -1.f }; + float branchside { -1.f }; +}; + +// 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_view const Text, bool const Switch ) { + + auto const fields { read_fields<6>( Text ) }; + attribute result; + result.value = std::max( fields[ 0 ], 0.f ); + if( Switch ) { + result.branch = fields[ 1 ]; + result.up = fields[ 2 ]; + result.side = fields[ 3 ]; + result.branchup = fields[ 4 ]; + result.branchside = fields[ 5 ]; + } + else { + result.up = fields[ 1 ]; + result.side = fields[ 2 ]; + result.cross = fields[ 3 ]; + result.dip = fields[ 4 ]; + } + return result; +} + +// in the layout of the entry it goes into, which can be of another type than the track the values come from +inline std::string write_attribute( attribute const &Values, bool const Switch ) { + + auto const fields { Switch ? + std::array { Values.value, Values.branch, Values.up, Values.side, Values.branchup, Values.branchside } : + std::array { Values.value, Values.up, Values.side, Values.cross, Values.dip, -1.f } }; + if( Values.value <= 0.f && std::all_of( fields.begin() + 1, fields.end(), []( float const Field ) { return Field < 0.f; } ) ) { + return {}; + } + return "uneven " + write_fields( fields ) + ' '; +} + +// 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 { + given( Values.up, Values.value ), + given( Values.side, Values.value ), + Plain ? given( Values.cross, Values.value ) : 0.0 }; + if( Path == 1 ) { + result.up = given( Values.branchup, given( Values.branch, result.up ) ); + result.side = given( Values.branchside, given( Values.branch, result.side ) ); + } + return result; +} + +template +using wave_set = std::array; + +// the waves run in all directions of the plan, so no direction of a track is favoured +template +wave_set make_waves( double const Turn, std::array const &Lengths, std::array const &Shares ) { + + auto const golden { glm::pi() * ( 3.0 - std::sqrt( 5.0 ) ) }; + wave_set result {}; + for( std::size_t i = 0; i < Count; ++i ) { + auto const number { glm::two_pi() / Lengths[ 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; +} + +inline constexpr std::array wave_lengths { 5.3, 7.1, 9.7, 13.1, 17.9, 23.9, 31.7, 41.9 }; +inline constexpr std::array wave_shares { 0.0721, 0.0997, 0.1344, 0.1703, 0.1549, 0.1386, 0.1226, 0.1074 }; + +// fewer short ones than sideways: the rails come near the limit in the vertical more often, with the peak still at it. +// The long ones hardly count on the 10 m chord: the body rises and falls on them, slowly enough for a pantograph to keep up +inline wave_set<8> const up_waves { make_waves<8>( 0.0, { 8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3 }, { 0.14, 0.19, 0.22, 0.22, 0.23, 0.23, 0.23, 0.23 } ) }; +inline wave_set<8> const x_waves { make_waves( 1.1, wave_lengths, wave_shares ) }; +inline wave_set<8> const z_waves { make_waves( 2.3, wave_lengths, wave_shares ) }; +// the tilt along each axis of the plan. Long waves only: with the short ones the cross level reaching its limit would break the limit of twist +inline wave_set<3> const tilt_x_waves { make_waves<3>( 0.7, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; +inline wave_set<3> const tilt_z_waves { make_waves<3>( 2.9, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; + +template +double sum( wave_set const &Waves, glm::dvec3 const &Point ) { + + auto total { 0.0 }; + for( auto const &wave : Waves ) { + total += wave.share * std::sin( wave.x * Point.x + wave.z * Point.z + wave.phase ); + } + return total; +} + +// displacement at the point for an amplitude of 1, sideways within +-1, up and down within +-1.69 +inline glm::dvec3 field( glm::dvec3 const &Point ) { + + return { sum( x_waves, Point ), sum( up_waves, Point ), sum( z_waves, Point ) }; +} + +// how much lower the rail towards Across lies by settling apart, for an amplitude of 1. A tilt of the plan, so tracks laid towards each +// other meet at one cant. Follows the middle of a path, so plain tracks only +inline double level( glm::dvec3 const &Point, glm::dvec3 const &Across ) { + + return cross_share * ( Across.x * sum( tilt_x_waves, Point ) + Across.z * sum( tilt_z_waves, Point ) ); +} + +// roll made by the field for an amplitude of 1; positive lowers the rail towards Across, as the roll of a path +inline double roll( glm::dvec3 const &Point, glm::dvec3 const &Across, double const Gauge ) { + + return ( sum( up_waves, Point - Across * ( 0.5 * Gauge ) ) - sum( up_waves, Point + Across * ( 0.5 * Gauge ) ) ) / Gauge; +} + +// value at a distance along a track: Full inside, exactly Atstart and Atend at its ends where it meets the neighbours +inline double along( double const Full, double const Atstart, double const Atend, double const Distance, double const Length ) { + + auto const smooth = []( double const Share ) { + auto const share { std::clamp( Share, 0.0, 1.0 ) }; + return share * share * ( 3.0 - 2.0 * share ); }; + auto const ends { Length > 0.0 ? Atstart + ( Atend - Atstart ) * smooth( Distance / Length ) : Atstart }; + return ends + ( Full - ends ) * std::min( smooth( Distance / fade ), smooth( ( Length - Distance ) / fade ) ); +} + +// how much of the displacement a point of the profile takes, by its depth under the rail top: all of it down to the foot of the rail, +// none at the foot of the ballast, which stays on the ground as designed +inline double bed_share( double const Depth, double const Rail, double const Height ) { + + return Height > 0.0 ? std::clamp( 1.0 - ( Depth - Rail ) / Height, 0.0, 1.0 ) : 1.0; +} + +// a displacement with the part along the track taken out, Across being a unit vector in the plan. Along the track +// it would only change the distance between coupled vehicles, and with it the forces between them +inline glm::dvec3 across_only( glm::dvec3 const &Offset, glm::dvec3 const &Across ) { + + return Across * glm::dot( Offset, Across ) + glm::dvec3 { 0.0, Offset.y, 0.0 }; +} + +// the unit vector across a direction in the plan, or across the chord if it has none; zero for a path of no length, which has neither +inline glm::dvec3 across_of( glm::dvec3 const &Direction, glm::dvec3 const &Chord ) { + + for( auto const &along : { Direction, Chord } ) { + glm::dvec3 const across { -along.z, 0.0, along.x }; + if( glm::dot( across, across ) > 0.0 ) { + return glm::normalize( across ); + } + } + return glm::dvec3 { 0.0 }; +} + +// 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 ) { + + if( Switch == Theirswitch ) { + return 0.5f * ( Own + Their ); + } + return Switch ? Their : Own; +} + +// each component the same way +inline amounts along( amounts const &Full, amounts const &Atstart, amounts const &Atend, double const Distance, double const Length ) { + + return { + along( Full.up, Atstart.up, Atend.up, Distance, Length ), + along( Full.side, Atstart.side, Atend.side, Distance, Length ), + along( Full.cross, Atstart.cross, Atend.cross, Distance, Length ) }; +} + +// distance from the joint behind the point +inline double rail_phase( double const Along, double const Rail ) { + + auto const phase { std::fmod( Along, Rail ) }; + return phase < 0.0 ? phase + Rail : phase; +} + +// share of the dip depth at a distance from the joint +inline double dip( double const Fromjoint ) { + + if( Fromjoint >= dip_reach ) { + return 0.0; + } + auto const height { std::cos( glm::half_pi() * Fromjoint / dip_reach ) }; + return height * height; +} + +} // namespace track_uneven diff --git a/world/TrkFoll.cpp b/world/TrkFoll.cpp index c437553da..035e0bd96 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -267,6 +267,11 @@ bool TTrackFollower::Move(double fDistance, bool bPrimary) } }; +double TTrackFollower::RailPhase( double const Offset ) const { + + return pCurrentTrack != nullptr ? pCurrentTrack->rail_phase( fCurrentDistance + fDirection * Offset ) : -1.0; +} + bool TTrackFollower::ComputatePosition() { // ustalenie współrzędnych XYZ if (pCurrentSegment) // o ile jest tor diff --git a/world/TrkFoll.h b/world/TrkFoll.h index bba97681a..62f386a95 100644 --- a/world/TrkFoll.h +++ b/world/TrkFoll.h @@ -39,6 +39,10 @@ public: // inline double GetLength(vector3 p1, vector3 cp1, vector3 cp2, vector3 p2) //{ return pCurrentSegment->ComputeLength(p1,cp1,cp2,p2); }; // inline double GetRadius(double L, double d); //McZapkie-150503 + inline glm::dvec3 UnevenBend() const { + return pCurrentSegment ? pCurrentSegment->uneven_bend( fCurrentDistance ) : glm::dvec3 { 0.0 }; }; + // TTrack::rail_phase of the point Offset ahead of the axle along the vehicle + double RailPhase( double const Offset ) const; bool Init(TTrack *pTrack, TDynamicObject *NewOwner, double fDir); void Reset(); void Render(float fNr);