17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-07 23:42:29 +01:00
Commit Graph

739 Commits

Author SHA1 Message Date
d4120ea5db Merge branch 'master' into feat/renderers-cleanup 2026-11-07 16:44:51 +01:00
jerrrrycho
466cbbbfa1 Test/core unit coverage (#235)
* test: real vehicles and golden traces in the unit tests

The game's own vehicles (dynamic/ of the release used for the tests) as
JSON of what the game's vehicle loader reads of their .fiz files, includes
expanded: test_fiz.cpp converts them on demand and checks the loader reads
the same sections, values and rows as of the game's file (fiz.h). Chosen
to cover every engine type, brake valve, brake handle, local brake,
coupler type, door type and brake delay set the game data uses.

Golden traces (golden.h) characterise the simulation, compared within a
relative tolerance or exactly, re-recorded with
EU07_UNIT_TESTS_UPDATE_GOLDEN=1:
- the parameters of every real vehicle as loaded,
- the brakes of every real vehicle through charge, service steps,
  release, overcharge and emergency, on every brake delay setting,
- every position of every driver's and local brake handle,
- every powered vehicle started up the way the AI does it, driven to
  full power and braked to a stop (drive.h),
- the real electric units and a locomotive with wagons driven whole, as
  trains of coupled vehicles stepped the way the game steps them
  (consist.h),
- scenarios of the controls (scenario.h): doors, reversing and cab
  change, slip and sand, emergency stops, releasers, the ep and the
  electrodynamic brake, heating, loading, a collision, and frames of
  several physics iterations.

* test: commands passed between vehicles

Every command RunCommand knows run on every real vehicle and recorded,
commands travelling through a train with a vehicle facing backwards, a
coupling without the control cable, and loading and unloading by commands.

* test: every switch of the cab, sent to the next vehicle

Each switch sending its state to the train, each way and to each range,
with the controllers and direction among them, on every real vehicle
driving another of its kind, recorded for both.

* test: the switches of the cab worked on a pair under the catenary

The same switches with two seconds for the vehicles to act on each, so
the pantographs rise, the main switch closes and the auxiliaries start.

* test: gauges of trains in multiple and the damage descriptions

The ammeters and the brake force the AI expects, recorded on the electric
units and locomotives in multiple driven, the damage described on the
driver's panel and ammeters showing the current of controlled vehicles.

* test: give load commands their place, as the game does

The loading test set its commands with SetInternalCommand, which leaves
TCommand::Location as it is: uninitialised in a fresh vehicle, so the
distance check of Load= and UnLoad= read garbage and the test failed
now and then. The game gives them through PutCommand with the place of
the platform, and so do the tests now.

* test: brake valves and handles no vehicle of the game data has

Fitted to a wagon and to an EU07: the valve variants and handles the
loader knows that no real vehicle uses, the CV1 ones and the test handle
among them, and a name the loader doesn't know.

* test: braking from speed with the automatic ED brake, and a draisine

Trains with the EP09, the E6ACT and the EW58 braked by ep, light and full
service applications from speed, and the draisine pumped from rest and
rolling. Only the E6ACT brings its electrodynamic brake in here.

* test: the EP09's electrodynamic brake in service braking held to a stop

start_up() took a vehicle for ready once its line breaker was closed and
its reservoir full, which a train made already moving is at once: the
EP09's manually started converter was never switched on, so there was
no 110 V and no electrodynamic brake. It now waits for the converter.

The EP09 with wagons is braked from 120 km/h by light, medium, high and
full service applications held to a stop: the motors brake, the air of
its cylinders is let out and given back as the train slows down. The
flag of the electrodynamic brake stays on at a standstill below 2.1 bar.

C strings given to CAPTURE are shown as text.

* test: the converter switched on before a train is ready, the EW58's ep-staged electrodynamic brake

start_up() called a train ready as soon as its line breaker closed and the air was there, so the converter
of a unit having it in another car than the cab (the EW58's middle car) was never switched on, and its
electrodynamic brake, gated by the 110 V, never came in. The converter and the compressor now always get
switched on first. The EW58 brakes electrically by the stages of the ep brake, not by the pipe alone.

* test: the ET42 and the 214M railbus among the real vehicles

The ET42's halves (its own series motor branch: the motors in series at the high current setting) and the
214MA, a diesel railbus with the hydraulic torque converter, the retarder and the controller of an induction
motor drive, converted from the game data and recorded in every sweep of the real vehicles.

* test: the wagons' hand brakes released with the train's brakes; the 754, SM31 and SU46

release_brakes() released the hand brake of the driver's vehicle only, so every wagon set up braked kept its
hand brake on and got dragged along. It now releases the hand and spring brakes of the whole train, as
TController::AutoRewident() does when the AI takes it over, and the traces of the trains with wagons are
recorded anew.

The 754, the SM31 and the SU46 join the real vehicles for the kinds of the diesel-electrics' field shunting
relays the others lack (RelayType 0, 1 and 46).

* test: traction on a lost and too high catenary voltage, the ET42, the diesel-electrics' shunting relays

The line breaker opened by the voltage relays of a series motor locomotive and by the limits of an induction
motor one. The ET42 at the high current setting, which keeps its halves' motors in series, and its switched
electrodynamic brake, whose current trips the overload relay from the top speed of the low setting. The field
shunting of the diesel-electrics through each kind of their relays, at full power and notched back.

* test: the EW58's electrodynamic brake stages, the EU47's speed control and braking at the voltage's limit

The stages of the EW58's motor brake set by the positions of its ep handle, the first one alone over
100 km/h. The EU47's second controller holding, raising and lowering the speed set, its local brake setting
the flag of the electrodynamic brake without the motors braking, and its brake feeding back less current into
a catenary over the drive's MaxV.

* test: diesel engines too cold and too hot to start, the 214M railbus's controller and retarder

The heat of the diesel engines: at -10 degrees the ones with the lowest water temperature set don't start
until the heater warms them, at 88 degrees the ones with a second water circuit don't start while it cools,
and the SM42 run hot at full power is kept under its highest temperature by its fans and shutters. The 214M
railbus's controller from the air brake through the retarder and neutral to power, its torque converter's
lock-up and its gearbox.

The controller helpers of the traction tests (full power, waiting for the line contactors, and notching back)
move to test::scenario.

* test: couplers coupled, uncoupled, broken and colliding; loads assigned, loaded and unloaded

Coupling by hand touching or apart and of the same kind or not, with the sounds of what changes; uncoupling a
screw coupler pressed together, stretched, with the hoses alone or broken; the automatic couplers of electric
units coupling on contact, without the control cable between two kinds; a coupler broken with its brake
hose; collisions over 15 km/h derailing. The pantographs set up by the pantstate load, a load of one kind at
a time up to the capacity, and the steps of loading and unloading.

* test: the battery drained and charged, the pantographs' tank and pressure switch, the 214M's speed control

The EU07's battery drained without the converter by the lights and the small compressor until it opens the
line breaker and nothing starts, charged by the converter, and switched off. The pantographs on their own
leaking tank until the pressure switch opens the line breaker, and the small compressor pumping it up and
priming the switch. The 214M railbus's speed control set by its buttons, steps and power limit, braking down
with the retarder, and standing by.

* test: the MHZ handles' buttons, the valves at speed, every brake material, the NESt3's relays

The overload and the high pressure buttons of the MHZ handles. The KE and the LSt at speed in the R and P
settings, under the anti-slip request and the releaser button. Every material of the brake shoes and disks
the loader takes, braking a wagon from speed, the ones no game vehicle has among them. The CV1_R and the
Oerlikon valve's relays set by its name (-ED, AL2 without ESt3) fitted to a wagon.

* test: the loader's lists and lines: the 218M, SA134 and 22WEc, a compressor list, Pmax by speed, skipped lines

The 218M (V2NList), the SA134 (HTCList) and the Elf 22WEc (ffBrakeList) join the real vehicles for the lists
the others lack. Definitions made by the tests for what no game file has: a compressor list, whose table of
whole numbers reads a factor of 0.9 as 0; the compressor's limits apart for the rear cab; the highest power
of an induction motor drive by the speed, extrapolated past the table; and the lines the loader skips or
trims (comments, a hash anywhere, leading spaces, carriage returns).

* test: what the cab and the AI call on a vehicle: gauges of the train, the split ED brake, spring brake, cut-off

The engine rotation of up to three vehicles in multiple and the lamps of the overload relay and the starting
resistors read through the control cable; the engine's idle speed and the ET42's second controller shown in
its dynamic brake; the controller of the electrodynamic brake apart from the local brake, which no game
vehicle has yet; the spring brake shut off and released, the ep brake's fuse and the handle stepped by the AI;
damaged motors cut off; the cab activated by itself; a road vehicle moved across a road, for which the tests'
track gets a width; and the lamps and the buzzer of the security system left unattended.

* test: the EP05 and its camshaft among the real vehicles, the headlights' colour and brightness

The EP05, whose camshaft closes the line breaker and the line contactors by the camshaft's own position,
converted from the game data and recorded in every sweep of the real vehicles. The Headlights section, which
only the 214M's own file has, in a definition made by the test.

* test: what the unit tests reach of the simulation core, what's dead or left, and what they found

COVERAGE.md: the blocks of Mover.cpp, hamulce.cpp, Oerlikon_ESt.cpp and friction.cpp the tests run, the code
nothing in the game runs, the code only data no game vehicle has would run, what's reachable and left, and
the behaviour the tests found that may deserve a look.

* test: check invariants of the physics on every step of every golden trace

Rules any vehicle keeps whatever a test does (no NaN, cylinders and pipes
within their limits, no current with the line breaker or contactors open,
no force without power, distance as the speed runs, no acceleration over
5 g), checked on every step, with no comparison to a recorded file. The
traces where a test or a game vehicle breaks one are excused with the
reason; the vehicles' cases are in the findings of COVERAGE.md.

* test: key figures of every golden trace, per part of the run

Duration, speeds, distance, time to stop, the brake pipe's lowest
pressure and time to recover, the peaks of the cylinders, the motor
current and the tractive force, for each part between two events, in
data/golden/<name>.figures. Compared within 0.5% or the last decimal, so
they hold on any compiler, and an intended change of behaviour shows in
a few readable lines. Traces sampled by hand keep none.

* test: keep the golden samples out of the repository, recorded on the code before a change

The samples of the golden traces are 29 MB and only mean something against
the code before the change they check, so they're recorded locally before it
(EU07_UNIT_TESTS_UPDATE_GOLDEN=1) and compared after it, and ignored by git.
The key figures and the invariants stay, and are what the CI checks.

Without samples recorded a trace compares only its key figures, but with
EU07_UNIT_TESTS_GOLDEN_EXACT=1, the check of a change which shouldn't alter
anything, it fails, so the check can't pass by having nothing to compare.

* fix: draw the noise of the control pipe pressure in one order on every compiler

The order of the two Random(10) calls in 'Random(10) - Random(10)' is
unspecified: GCC draws them the other way round than MSVC Release, which
mirrored the control pipe pressure around 5 bar and failed 32 unit tests
on the Linux CI. The draws are sequenced in the order of MSVC Release,
which the golden traces were recorded with.

* test: count the start-up rounds with an integer, not a double (SonarCloud S2193)

* test: behaviour in CHECKs, the key figures out of the repository

The key figures of the golden traces are recorded with their samples on the code
before a change, and compared after it, instead of being kept: a trace without
them checks the invariants and its test's CHECKs, which is what the CI runs.

The scenarios of the controls state what they show in CHECKs, and the runs of
every real vehicle what holds for each of them. Writing them showed scenarios
which didn't do what they were named for: the cab change stopped in the engine
room, the slip runs put the controller to full power at once and never got the
line contactors closed, and the train stopped by the alarm chain never got its
pipe charged again. They do it now; the rest went to the findings.

* test: the SonarCloud findings in the unit tests

std::format for the texts put together, emplace_back, const references, string_view parameters, std::array and std::vector for the C arrays, the ranges algorithms, names no longer shadowing the coupler ends, comments in the empty loops; start_up(), trace::check() and the converter of the definitions split up. BrakeForceR() is const, which it always was in effect, so the gauges test needs no const_cast. The golden traces compare bit for bit.
2026-11-07 12:37:12 +01:00
jerrrrycho
e0cef91049 fix(physics): running backwards, the camshaft goes back down from the last series position (eu07/maszyna#393) (#256)
Running backwards, AutoRelayCheck() keeps the camshaft from joining the motors in more branches than
BackwardsBranchesAllowed (the series connection alone, by default). It did that by returning before any move of
the camshaft, so once it reached the last series position (22 of 43 in the Skoda 181 and 182, the next one joins
the motors in two branches) it stayed there whatever the driver did with the master controller: with the
controller back at zero the line contactors stayed closed and the motors kept pulling, until the line contactors
were opened by hand. Now the limit only keeps the camshaft from going further up; it goes back down as usual.

Tests: tests/unit/test_camshaft.cpp, on a locomotive made after the Skoda 181 (its RList); the test definitions
take the RList and MotorParamTable0 tables.
2026-11-07 04:48:15 +01:00
pawelw
fc22645d7c fix(physics): articulated couplers stall freight trains (eu07/maszyna#553) (#255)
* fix(physics): articulated couplers stall freight trains (eu07/maszyna#553)

Articulated wagons (T3000e, N-230, N-234) set up with CType=Articulated
barely moved in a freight train and jerked.

- The articulated joint took the spring force up to DmaxC = 0.05 m only,
  225 kN, well short of its FmaxC of 850 kN and of what a loaded section
  in a freight train takes. It overstretched and the sections collided at
  every physics step. Dmax is now sized so the spring carries FmaxC
  (0.19 m stretched, 0.09 m pressed in), never below the old 0.05 m.
- ComputeCollision() scaled the velocities of both vehicles by
  sqrt(1 - beta), so each collision braked the pair as a whole: two
  coupled vehicles in a sustained "collision" lost a third of their speed
  per step. The restitution now applies to the velocities relative to the
  centre of mass only, and the pair keeps its momentum.

Tests: the articulated joint carries FmaxC within its travel; a jerked
articulated set and a jerked screw-coupled pair keep their momentum. On
the code before the fix both pairs stop dead within 3 s.

* style(physics): English-only comment in ComputeCollision()

The comment above the collision velocities started in Polish and went on in English.

* style(physics): SonarQube findings in the #553 fix

std::lerp for the collision velocities (cpp:S6179) and test locals renamed so they no longer shadow the end and coupling enumerators (cpp:S1117).
2026-11-05 10:25:26 +01:00
jerrrrycho
d995674bee fix(physics): automatic couplers don't catch above the safe collision… (#217)
* fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds

fix(cab): the line contactors lamp goes out with the pressure switch (eu07/maszyna#470)

The lamp of the open line contactors, which tells the start can be made, went out at 1 bar in the brake
cylinders, while the pressure switch cuts the start off only at 2 bar (or below 3.6 bar in the main pipe), so
with the brakes partly applied the start could be made with the lamp out. With a pressure switch fitted the lamp
now follows it; without one it goes out at 1 bar as before. The same holds for the lamp of the second unit, which
used to keep its last state between 1 bar and the cut-off. The condition is
TMoverParameters::LineContactorsLampLit().

Tests: tests/unit/test_line_contactors.cpp.

refactor: Sonar findings of PR 217

- horn_supply::pneumatic shadowed the pneumatic enumerator of the sound flags (cpp:S1117), it's horn_supply::air;
  the supplies read with using enum (S6177),
- CouplerForce(): the automatic coupling refused or caught without another level of nested ifs (S134), the
  same outcome: too fast, the coupling is locked; caught, coupled and locked; otherwise left to try again,
- CompressorCheck(): the power the compressor doesn't draw yet noted without a TODO (S1135),
- tests: the previous crash damage setting in an in-class initializer (S3230), the loops which stop early with
  break rather than in their conditions (S886), the stand-ins of test::pair in a std::vector rather than an array
  (S5945), units in the comments of the example without /* */ (S1103).

test: the cases the changes of the branch left without a test

- Cntrl.: BDelay1 to BDelay4 reach the brake delays (now through std::format), the default start types of the
  ground and converter overload relays of a multiple unit and of other vehicles (out of the nested conditionals),
- the compressor: fed from the low voltage it waits from the low voltage coming on; the pressure switch stops it
  above the cut-out pressure, without one the excess air is vented (the vented fraction rewritten),
- the horns: a vehicle without any air sounds as before, the threshold pressure, an electric horn on 110 V,
- the positions of a cab control below zero, and a control without position sounds.

refactor(cab): the kind of the train heating switch as a flag

Read once with the vehicle definition, rather than compared as text in each of the switch commands.

feat(cab): train heating switch springing back (MaSzyna-EU07/maszyna#85)

TrainHeating=[toggle]|impulse in the Switches: section of the vehicle definition makes the train heating switch
an unstable one: it goes up to switch the heating on, down to switch it off, and back to the middle once let go,
instead of staying where it was put. The position of such a switch isn't set from the heating state when the cab
is entered.

Tests: tests/unit/test_switches.cpp.

feat(sound): mirror sounds (MaSzyna-EU07/maszyna#84, eu07/maszyna#322)

The sound definitions of a vehicle (.mmd) take mirroropen: and mirrorclose:, played as a mirror sets off
unfolding or folding, and the locations section mirrors: <offset>... end, the offsets along the vehicle of the
mirror pairs, as for the doors (negative ahead of the centre), each pair at the sides of the vehicle. As the
animation, only the mirrors at the occupied cab sound. The start of a move is mirror_moves::motion in
vehicle/mirrors.h.

Tests: tests/unit/test_mirrors.cpp.

feat(cab): joint oil and fuel pump switch (MaSzyna-EU07/maszyna#81)

A cab control oilfuelpump_sw: switches both the oil and the fuel pump at once, with the new command
oilfuelpumptoggle (no key by default). As a two-state switch it turns both pumps off when both are on and both on
otherwise; as an impulse switch it's held to start them, like the separate pump switches. Its position follows
the pumps when the cab is entered.

refactor(editor): text fields filled without strncpy (MaSzyna-EU07/maszyna#112)

The last three strncpy calls Sonar flags (cpp:S5816), in the road and track editors, copy the text into the
ImGui field buffers with std::string::copy and terminate it right after; the text gets cut to the buffer as
before.

fix: automatic coupler crashes, horns, compressor start delay, radio and cab lever sounds

- automatic couplers don't catch above the safe collision speed (#86): with crash damage on they couple at a
  closing speed of 15 km/h at most, a faster hit locks the coupling until the vehicles part and the collision
  runs its course. On the way: CollisionVelocity() read a head-on 10 + 10 km/h as 14.1 km/h (quaternions turn
  by half the angle), it's now taken from the touching ends; CollisionDetect() took the other vehicle's speed
  the wrong way round for vehicles facing each other. Head-on hits now derail sooner, at the intended limit,
- a single horn lever can't sound both tones at once (eu07/maszyna#392),
- a skid ends once the vehicle stops on locked wheels (eu07/maszyna#479),
- electronic destination signs are blanked at the end of the timetable (eu07/maszyna#446),
- horns need the air or the voltage to sound (eu07/maszyna#290, #76): HornType=[Pneumatic]|Electric and
  Horn3Type in Cntrl.; a pneumatic horn needs 0.2 MPa in the main reservoir (or the main pipe without one),
  an electric one the low voltage; the lever stays where it was put,
- compressor start delay (#67): CompressorStartDelay [s] in Brake., counted from the compressor getting its
  power, after the engine starts say; none by default,
- the channel and volume buttons of a radio without power do nothing (eu07/maszyna#429); the power check of
  the radio is TMoverParameters::RadioPowered(),
- a lever moved smoothly sounds the positions it passes (#83).

Sonar findings in the functions changed: nesting, nested conditionals, commented-out code, std::format,
references to const. Unit tests: test::pair for two vehicles running into each other, test_couplers,
test_horn, test_wheelslip, test_compressor, test_radio, test_gauge_positions.

* fix: review findings of PR 217

- the pressures of the mover are in bar (MinCP=7.0 in the vehicle definitions), so the 0.2 the pneumatic horns
  needed was 0.2 bar; it's 2 bar now, and the tests take their pressures in bar too,
- the lamps of the ground relay and of the motor overload relays follow the pressure switch as the line
  contactors one does, TMoverParameters::MainCircuitLampsEnabled(), so the lamps of one cab go out together,
- the joint oil and fuel pump switch leaves out a pump started automatically, which ignores the switching:
  with one, it couldn't turn the other off, and showed off with both running (OilFuelPumpsEnabled()),
- the kind of the train heating switch comes from the occupied vehicle on entering the cab too, as in the
  switch commands; in a multiple unit these are different vehicles,
- CouplerForce() put the force taken back by the other vehicle on its far coupler when the vehicles faced each
  other (Couplers[1 - End] rather than the coupler it's coupled with),
- the velocity difference along the coupler axis computed once, CouplerVelocityDifference(), for CouplerForce()
  and CollisionVelocity(),
- the attachcoupler flag added without the SetFlag() call with 0, the index of the mirror moves noted as by side.

Tests: the coupler force of vehicles facing each other, the joint switch with an automatic pump, the lamps of
the main circuit, the horn and compressor pressures in bar.

* fix: coupler force of vehicles facing each other taken back along the other vehicle's axis

CForce runs along the axis of its own vehicle, so the force the other vehicle takes back on its coupler of the pair
is turned around once more when the vehicles face each other (-CF * DirPatch()), as that vehicle computes it itself.

Tests: the coupler forces of the pair facing each other and facing the same way.
2026-11-01 09:12:36 +01:00
2f96dfe767 perf(physics): air flowing along a pipe wakes the physics of a coupled vehicle only when it changes its pressure by more than physics.pipewake (0.001 bar), not with any trickle of it 2026-10-23 19:30:21 +02:00
9a7ff372f7 perf(debug): count what wakes the physics of the vehicles (brake pipe, main reservoir pipe, command, speed, acceleration, neighbour, other) in the debug panel 2026-10-23 19:08:09 +02:00
f17462b186 perf(physics): road vehicles farther than 1 km from the camera go by one step of the physics in a frame (physics.roadlod); count what resets the switching time of the vehicles (compressor, mains, diesel, physics woken) in the debug panel 2026-10-23 18:47:01 +02:00
a04efbf968 perf(physics): vehicles standing still and held by their brakes let their physics sleep (physics.sleepheld) 2026-10-23 18:20:49 +02:00
maj00r
d25f20a56c feat(track): 3D sleepers and switch timbers, blade heel and throw from templates, one rail carving pass
- sleepers3d / sleepers3dplain / sleepers3dbed in eu07.ini: timbers put together from the begin, main and end parts of a model
  and stretched to their length, plain sleepers of their own model; tracks whose plain sleepers would touch are laid as one
  cluster, lines of timbers led by priority and turned to the bisectors of frogs and crossings at a limited rate, whole timbers
  under crossings, split ones between tracks with room to part them, gaps filled per rail and overlaps shortened or dropped;
  clusters worked out in a background thread while driving, on demand in the editor
- the ballast can be left out under the beams (sleepers3dbed no)
- blades get their heel and throw from the switch template or blades <heel> <throw> in the scenery; blades of double switches
  run on into the track linking them to the next switch
- rail cutting at frogs, wing rails, movable noses and switch crossings done by one carving pass (RailCarve)
- tint of the rail treads applied to the colour only, without the matte band at the frog
- a switch trailed through with a movable nose set the other way derails the vehicle
- self-tests: sleepercheck, sharedzones, beamsnear, throw, sleepersbg
2026-10-17 23:32:43 +02:00
jerrrrycho
b400255dae Test/physics unit tests (#213)
* build: doctest 2.4.11 in ref/

Single header test framework (MIT), vendored like the other libraries in
ref/ so the tests need nothing extra installed on the CI runners.

* refactor(physics): read vehicle definitions from text as well as files

TMoverParameters::LoadFIZ(path) opens the file and hands the parser over
to LoadFIZ_Parse(), which holds what LoadFIZ() did with it before. The
new LoadFIZ_Text() reads the same format from a string, so a definition
can be made up in memory, e.g. by a unit test. No change in behaviour for
files.

* build: the simulation core as a library the game links

The vehicle physics (McZapkie), the vehicle definition parser and the
helpers they use build as eu07_core, a static library the game links,
so the unit tests to come can link the very same objects instead of
compiling their own copy with their own settings.

The game's build settings (include dirs, definitions, compile options,
libraries) move from the executable to eu07_settings, an interface
target the library links publicly, so the game, the library and later
the tests all build with the same ones. What only the executable needs
stays with it: /MAP, /LARGEADDRESSAWARE, output dirs, the precompiled
header, runtime DLL copying, the crash reporter and the experimental
renderer. No change in what the game builds or links.

* test: unit tests of the simulation core, run as part of the build

eu07_unit_tests: C++ tests (doctest) of the simulation, which set up the
conditions, run the code and check the outcome. No game data, no window.

- the code under test is the core library (eu07_core), the very objects
  the game links, built with the game's settings; engine_stubs.cpp stands
  in for the logs, settings and scenery layers the library reaches for.
  python gets linked with the settings but isn't needed to run the tests
- test vehicles are JSON definitions in tests/unit/data, with the sections
  and keys of the game's vehicle definitions (tables as "rows", _keys are
  comments), changed by the tests where needed and loaded from memory by
  the game's own loader; an unknown section is an error rather than
  silently skipped
- test::vehicle creates the mover the way TDynamicObject::Init() does and
  steps it the way the simulation loop does, on a straight track of given
  friction and gradient, optionally holding the brake pipe pressure the
  way a coupled locomotive would
- the random numbers of the simulation are reseeded before every test
  case, so a test gives the same result alone (IDE) or among the others
- building the game runs the tests at the end, once the game is linked,
  and prints a one line summary ("Unit tests: 11/11 passed (80 checks)").
  A failure prints the full report and fails the build, in CI and locally
  alike, and removes the game binary so the next build reruns them. A
  change of the tests alone relinks the game so they run (Ninja and
  Makefile generators). -DWITH_TESTS=OFF leaves them out
- first tests: definitions reach the vehicle, KE service application and
  release, deeper reduction brakes harder, every implemented distributor
  valve charges/applies/releases, service braking stops a moving wagon

* test: stub basic_node::radius_() for GCC and Clang

The Linux build failed to link the unit tests: undefined reference to
scene::basic_node::radius_() from basic_node's vtable in engine_stubs.o.
basic_node has no key function for the Itanium C++ ABI (tooltip() is
declared plain but defined inline in the header), so GCC and Clang emit
its vtable in every unit using scenenode.h, and the vtable needs
radius_(), defined in scenenode.cpp, which the tests don't link. MSVC
emits vtables differently, so the Windows build linked.

* test: test vehicle definitions load with GCC and Clang

All the tests reading a definition failed on Linux with "unknown vehicle
definition section 0": the parsed definition was brace-initialised
(auto const json{parse(...)}), which GCC and Clang take for the
initializer list constructor of nlohmann::json, making an array holding
the definition, while MSVC copies it. Initialise it with =, and require
the definition to be a JSON object.


* test: SonarCloud issues of the unit tests

- test::definition_error for unusable test vehicle definitions instead of
  std::runtime_error (S112)
- the rows of a table section written by a function of their own, less
  nesting in definition::text() (S134)
- std::less<> for the section table (S6045)
- comments in the empty methods of the reseeding listener and stub (S1186)
- NOSONAR where the engine's headers fix the stubs: the extern globals
  and the member function signatures (S5421, S5817, S1238)
- NOSONAR on LoadFIZ_Parse(): the cognitive complexity is the former body
  of LoadFIZ(), moved as it was (S3776)
- a comment Sonar took for code reworded (S125)
2026-10-15 17:56:41 +02:00
combolek
3ed864812f Fix line endings (#211)
There where some extra CRs introduced in a recent commit.
2026-10-11 20:42:09 +02:00
jerrrrycho
6263fe539b Fix/sonar maintainability rebased (#209)
* refactor(sonar): use override instead of redundant virtual (cpp:S3471)

* refactor(sonar): remove empty statements, one declaration per statement (cpp:S1116, cpp:S1659)

* refactor(sonar): drop redundant lambda return types (cpp:S3574)

* refactor(sonar): prefer using over typedef (cpp:S5416)

* refactor(sonar): use =default for trivial special members (cpp:S3490)

* refactor(sonar): use nullptr literal (cpp:S4962)

* refactor(sonar): remove redundant access specifiers (cpp:S3539)

* refactor(sonar): remove redundant casts (cpp:S1905)

* refactor(sonar): use std::scoped_lock instead of std::lock_guard (cpp:S5997)

* refactor(sonar): construct elements in place with emplace_back (cpp:S6003)

* refactor(sonar): make narrowing conversions explicit (cpp:S5276)

* refactor(sonar): mark unused parameters (cpp:S1172)

Unused names are commented out; parameters used only on some platforms
are marked [[maybe_unused]].

* refactor(sonar): declare non-mutating member functions const (cpp:S5817)

Virtual functions are left untouched so overrides keep matching.

* refactor(sonar): merge collapsible nested if statements (cpp:S1066)

Comments that preceded the inner if are kept above the merged statement.

* refactor(sonar): scope single-use variables in if init-statements (cpp:S6004)

* fix(sonar): give polymorphic base classes virtual destructors (cpp:S1235)

* refactor(sonar): use try_emplace for map insertion (cpp:S6030)

* refactor(sonar): use auto where the type is spelled out on the right (cpp:S5827)

* refactor(sonar): remove redundant parentheses (cpp:S1110)

* refactor(sonar): document intentionally empty functions (cpp:S1186)

* refactor(sonar): remove dead stores (cpp:S1854)

Side effects (e.g. LISTLINE++) are kept.

* refactor(sonar): remove unused variables, captures and private fields (cpp:S1481, cpp:S1068)

* refactor(sonar): use structured bindings for pair access (cpp:S6005)

* refactor(sonar): use range-based loops and ranges algorithms (cpp:S5566)

* refactor(sonar): split chained assignments (cpp:S1121)

* refactor(sonar): add default cases to switch statements (cpp:S3562)

* refactor(sonar): use std::size for array lengths (cpp:S7127)

* refactor(sonar): use std::ranges algorithms and std::erase(_if) (cpp:S6197)

* refactor(sonar): access static members through the class (cpp:S2209)

* refactor(sonar): add braces around nested if-else (cpp:S5261)

* refactor(sonar): avoid side effects inside typeid operands (cpp:S5279)

* refactor(sonar): drop top-level const from by-value return types (cpp:S5951)

* refactor(sonar): declare variables separately from their type definitions (cpp:S3646)

* refactor(sonar): declare loop counters in the for statement (cpp:S5955)

* refactor(sonar): rely on class template argument deduction (cpp:S6012)

* refactor(sonar): inherit base constructors instead of forwarding them (cpp:S5952)

* refactor(sonar): rename locals that shadow outer declarations (cpp:S1117)

Only local variables and definition parameters are renamed; shadowing
struct fields and enumerators in headers are left for a separate change.

* refactor(sonar): reduce enum qualification with using enum (cpp:S6177)

Applied only where no unqualified name in the block could be captured by
the introduced enumerators.

* refactor(sonar): mark converting constructors explicit (cpp:S1709)

sound_source and the gl::object conversion operators stay implicit; they
are relied upon by copy-list-initialisation and GL calls.

* refactor(sonar): initialise members in initialiser lists or declarations (cpp:S3230)

* refactor(sonar): make read-only local references and pointers const (cpp:S5350)

* refactor(sonar): take read-only pointer and reference parameters as const (cpp:S995)

TTrain command handlers keep their signature, it is fixed by the handler table.

* refactor(sonar): pass expensive-to-copy parameters by const reference (cpp:S1238)

Signatures declared in files touched by PR 206 (Driver.h, EU07.cpp) and
virtual overrides are left unchanged.

* refactor(sonar): use transparent comparators for string-keyed maps (cpp:S6045)

std::unordered_map instances and map types exposed in headers used by
other modules are left as they are.

* fix(sonar): do not use UI text as a printf format string (cpp:S5281)

Text lines containing '%' would otherwise be interpreted as format
specifiers by ImGui::TextColored.

* refactor(sonar): use std::bind_front for callbacks (cpp:S5995)

* refactor(sonar): drop const_cast on string literals passed to the Python C API (cpp:S859)

* refactor(sonar): take read-only string parameters as std::string_view (cpp:S6009)

The Windows messaging helpers keep std::string, they need c_str().

* refactor(sonar): make read-only global tables const (cpp:S5421)

Mutable global state (flags, timers, singletons) is left as is.

* refactor(sonar): replace single-case switch statements with if (cpp:S1301)

* refactor(sonar): format UUIDs and log timestamps with std::format (cpp:S6494, cpp:S5945)

* refactor(sonar): use std::array for local fixed-size arrays (cpp:S5945)

Arrays that are class members or part of headers are left unchanged.

* fix(sonar): test the bearing damage bit, not bit 0 (cpp:S872)

'DamageFlag & dtrain_bearing > 0' parsed as 'DamageFlag & (dtrain_bearing > 0)',
i.e. DamageFlag & 1, so bearing friction reacted to the wrong damage flag.

* refactor(sonar): assorted small fixes

- empty() instead of size() comparisons (cpp:S1155)
- member initialisers in declaration order (cpp:S3229)
- no redundant c_str() when a std::string is accepted (cpp:S7121)
- contains() on associative containers (cpp:S6171)
- const reference instead of a copying range-for (cpp:S6032)
- starts_with() for prefix checks (cpp:S6178)
- no std::move on temporaries (cpp:S5274)
- has_value() on std::optional (cpp:S7172)
- bool literals for bool members (cpp:S5820)
- inline only on the definition (cpp:S3548)
- explicit base access specifier (cpp:S5965)
- explicit element access instead of dereferencing an array (cpp:S7116)
- rely on the C++20 rewritten operator!= (cpp:S6186)

* refactor(sonar): more small fixes

- std::format instead of iostream manipulators in to_string/to_hex_str (cpp:S6495);
  output verified identical, negative hex values keep the unsigned form
- remove an empty compound statement (cpp:S108)
- drop an always-false unsigned < 0 branch (cpp:S1768)
- explicit lambda capture (cpp:S3608)
- std::make_unique (cpp:S5950)

* chore: strip stray carriage returns left in TTrain constructor comments

* fix: keep quantize() in its own value type

The S5276 change cast the result of the generic quantize<Type_>() to
unsigned int, which truncated float/double callers (shadow angle slider,
editor rotation snapping, AI speed control steps) and broke negative values.

* fix(sonar): use integer loop counters in segment sampling (cpp:S2193)

* refactor(sonar): clean up issues reported on PR 209

- remove locals left unused after the dead-store cleanup (cpp:S1481, cpp:S1854)
- unnamed unused parameter (cpp:S1172)
- single-use declarations moved into if init-statements (cpp:S6004)
- auto for casted initialisers (cpp:S5827)
- const references and pointers where nothing is modified (cpp:S5350)
- rename structured bindings shadowed by inner declarations (cpp:S1117)
2026-10-11 13:22:33 +02:00
jerrrrycho
79bd0c8e91 fix(sonar): addressing reliability/security issues (#202) 2026-10-04 20:00:13 +02:00
jerrrrycho
76a39af8c2 fix: bound MotorParam and MPTRelay indices (#196) 2026-10-01 20:21:01 +02:00
jerrrrycho
06078ddab9 fix: other sonar sast fixes (#193) 2026-10-01 13:30:25 +02:00
jerrrrycho
f05b58b0e9 Revert "fix: yet other sonar sast fixes"
This reverts commit 403a6d1f9f.
2026-10-01 08:01:23 +02:00
jerrrrycho
403a6d1f9f fix: yet other sonar sast fixes 2026-10-01 07:58:28 +02:00
pwarszawski
96be688d13 Send the reverser position, not the absolute direction, when activating a cab
CabActivisation() passed DirAbsolute (DirActive * CabActive) as the value of the
"Direction" command, which every receiving vehicle stores as its own DirActive.
Every other sender passes DirActive. With the cab at the rear end (CabActive = -1)
the two have opposite signs, so the other units got their reverser flipped
against the controlling one.

It shows whenever a rear cab is activated after the reverser has been set:
- the AI takes over an occupied cab (TakeControl() calls CabActivisation(true)),
  e.g. an EN57 inserted with reardriver and a starting velocity: the motor car
  drives the train backwards;
- a vehicle without automatic cab activation (SA134) driven by the AI from the
  start: the cab is activated by a driver hint after the reverser is set, and
  the two units pull against each other.

Fixes #169.
2026-09-30 15:00:12 +02:00
6befd69968 Merge pull request #149 from combolek/getpf
state_manager: Optimize computation of pressure flow for Oerlikon ESt valves
2026-09-04 16:39:49 +02:00
jakubg1
f8cb5e4431 Fix EP fuse auto-(dis)engaging
When the Autorewident function is enabled, the EP fuse switch will no longer automatically engage or disengage if the player is controlling the train. This would happen for example when approaching a station.
2026-08-24 00:24:15 +02:00
Ryba04
7cc6a67399 Followed sonar's suggestions 2026-08-18 20:34:37 +02:00
Ryba04
7d3b9a7360 Fix "Traxx" local brake 2026-08-18 00:04:31 +02:00
combolek
19441b5e91 Convert Przekladniki to std::array 2026-07-28 09:28:10 -07:00
combolek
12c583f418 Fix some SonarQube warnings 2026-07-28 08:54:33 -07:00
combolek
00bb44a789 state_manager: Optimize computation of pressure flow for Oerlikon ESt valves
TNESt3::GetPF() is a *major* hotspot in simulation state update. The leading cause is the use of typeid (RTTI) which is implemented in the C++ runtime essentially as a string compare. This change eliminates the use of typeid and instead caches statically typed pointers for the few TPrzekladnik subclass objects that need their parameters adjusted in TNESt3::GetPF(). As a bonus some virtual methods on TPrzekladnik can now be removed. Overall this change reduces the TNESt3::GetPF() contribution from about 25% to 16%.
2026-07-26 21:19:38 -07:00
maj00r
03bfbb1345 fix: restore min>max tolerance lost in clamp->std::clamp migration
The dumb3d->glm / utilities-simplification refactor replaced the old custom
clamp() (which tolerated Lo>Hi, returning the upper bound) with std::clamp,
where inverted bounds are undefined behaviour. Only a few sites were patched
afterwards, by hand, with std::minmax plumbing (incl. a stray `static` inside
a per-axle loop). Others silently produced wrong results in the AI braking /
acceleration path.

Add a safe_clamp() helper (normalizes inverted bounds) and use it ONLY where
Lo<=Hi cannot be proven at compile time (config min/max pairs, sign-dependent
expressions, container underflow). Sites with constant bounds or [0, x>=0]
keep std::clamp. Remove the manual std::minmax workarounds and rewrite the
damaged-track jolt code value-first.
2026-07-02 00:37:52 +02:00
jerrrrycho
d85096f64d reformat: remove redundant parentheses 2026-06-30 21:19:46 +02:00
jerrrrycho
bf147dd155 refactor: replace null with nullptr 2026-06-29 02:48:25 +02:00
docentYT
e06ae37f47 replace new clamps with std::clamp and to_string with std::string 2026-05-05 23:40:59 +02:00
docentYT
5367cb9940 Merge remote-tracking branch 'upstream/master' into utilities-simplification 2026-05-05 23:08:43 +02:00
cb4b6b9a4f Separate electrodynamic and localbrake levers 2026-05-03 21:51:16 +02:00
docentYT
13c7317f6e Do not allow implicit cast for inline std::string to_string(bool Value) and change to_string to std::string for non boolean values 2026-05-03 15:49:29 +02:00
docentYT
59347af5a0 Replace some std::min and std::max combinations with std::clamp 2026-05-02 19:38:14 +02:00
docentYT
ca699d117f Remove not needed includes 2026-05-02 14:40:31 +02:00
docentYT
3628eb0fc3 Replace interpolate with std::lerp and glm::mix 2026-05-02 01:31:38 +02:00
docentYT
d1f16411a1 Replace clamp with std::clamp 2026-05-01 22:14:29 +02:00
docentYT
ec1dfae953 Replace starts_with and ends_with with string class methods 2026-05-01 17:31:11 +02:00
docentYT
337e750ed1 Replace Max0R and Min0R with std::max and std::min 2026-04-30 23:47:32 +02:00
7acc34df3e Merge pull request #97 from docentYT/migrate-from-dumb3d-to-glm
Migrate from dumb3d to glm
2026-04-28 19:46:46 +02:00
docentYT
d5d1825e38 Remove dumb3d 2026-04-27 23:03:47 +02:00
129924315d Cleanup hamulce.cpp/h with Mover.cpp/h 2026-04-27 18:21:55 +02:00
dcfd34908a Revert "Add power smoothing for inverter based vehicles"
This reverts commit 40bbbc6210.
For some reason it broke SA13x controllers lol
2026-04-17 20:08:04 +02:00
593e4bcf36 Merge branch 'master' into master 2026-04-14 21:01:04 +02:00
ae056f2bbd Merge pull request #79 from xingzihai/engine-overheat
feat: add EngineMaxTemperature support for diesel locomotive overheat detection
2026-04-14 20:49:37 +02:00
jakubg1
0d010e8c6a Simplify derailing logic, derail reason enum
- The `TMoverParameters::derail()` function has been renamed to `::Derail()`, with a capital D.
- All derail events now use that function.
  - Simplified and unified the logic behind different derail types.
  - The `dtrack_freerail` track type is now slightly increasing the chance of derailing on a tight curve, as it was the intention.
- A new `DerailReason` enum has been created which holds all derail types.
- The `DerailReason` field in the `TMoverParameters` class has been removed.
  - This was a transient field which is not necessary anymore.
  - The logging functionality as such has been moved from `DynObj.cpp` to `Mover.cpp`.
2026-04-04 02:05:49 +02:00
xingzihai
754fc50ca8 feat: add EngineMaxTemperature support for diesel locomotive overheat detection
Add engine temperature monitoring to diesel locomotive heat management system:
- Add engine field to heat_data struct (fluid_circuit_t type)
- Add EngineMaxTemperature config extraction in .fiz files
- Add engine overheat detection logic (is_hot check)
- Include engine.is_hot in PT status check

Implements issue #77: Oil, water and engine overheat lamps for diesel locomotives
2026-04-01 23:28:28 +00:00
Agent
68fa78a582 feat(overheat): Add overheat indicator lamps for oil, water, water_aux, and engine
- Add engine_max_temp and engine_is_hot fields to heat_data structure
- Add EngineMaxTemperature field extraction from .fiz files
- Add engine overheat check comparing Ts with engine_max_temp
- Add four new overheat lamp indicators:
  - i-oil_overheat
  - i-water_overheat
  - i-wateraux_overheat
  - i-engine_overheat
- Add lamp update logic to turn on/off based on is_hot flags
- Add lamp initialization in clear_cab_controls()

This implements issue #77 - overheat lamps for diesel engines.

The lamps will turn on when:
- Oil temperature exceeds OilMaxTemperature
- Water temperature exceeds WaterMaxTemperature
- Auxiliary water temperature exceeds WaterAuxMaxTemperature
- Engine temperature exceeds EngineMaxTemperature

Each check includes 8-degree hysteresis to prevent rapid on/off cycling.
2026-04-01 23:26:39 +00:00
copilot-swe-agent[bot]
7e49aa8740 Rewrite #includes to use explicit subdirectory paths
Co-authored-by: Hirek193 <23196899+Hirek193@users.noreply.github.com>
2026-03-14 19:16:48 +00:00
40bbbc6210 Add power smoothing for inverter based vehicles 2026-02-08 13:04:49 +01:00