Changes to how the AI works the train brake on the way to a lower speed
or a stop, so that it uses few, early applications instead of a late
deep one:
- Approaching a lower speed, the deceleration may fall short of the
demand by only half of a step (fBrake_a1) before the brake is applied
harder. With a full step the demand crept up meanwhile and a long G
train got its next step many seconds too late.
- The shortfall is checked every 3 s. If it keeps growing at a handle
left alone for longer than the brake takes to respond, the AI adds a
small step at once instead of waiting until the margin calls for a
deep one. A shortfall that shrinks on its own (brake still filling,
blocks gripping harder at lower speed) does not trigger it.
- Releasing, a position short of the first one counts as giving the
minimum stage of the distributor, not less.
- Holding the speed on a descent, the predicted excess speed is taken
off over the time the brake takes to respond, not at once, so a slow
brake does not get a deep application that then takes as long to
release.
The rule "wyluzuj lokomotywe, to szybciej ruszymy" released the
locomotive's own brake whenever power was asked for and the locomotive
cylinder held more than 0.4 bar. It had no speed condition, so it also
fired in motion, at 50-75 km/h, right after an application. On a
locomotive with the EStED distributor (E186) this vented the control
reservoir; with the pipe not yet recharged the locomotive brake then
faded to nothing at the next application.
The rule is meant for starting off, so it now applies only below
1 km/h.
The SM03 had no way to stop its engine. Its fuel pump starts on its own, so the fuel pump switch is ignored; the
engine runs the converter feeding the low voltage circuit, so the battery switch doesn't starve the fuel pump either;
and its cab has only the starter button, for which TTrain::OnCommand_linebreakeropen() kept a legacy branch which
animated the button and returned without opening the circuit. MainSwitch(false) itself stops the engine fine.
The command now opens the circuit on diesels whose cab has only the starter button
(TMoverParameters::MainCircuitOpensWithStarterOnly()), without animating the button, which held down would start
the engine again. Other vehicles with only the button closing the circuit keep ignoring the command.
Tests: tests/unit/test_engine_shutdown.cpp, on a shunter made after the SM03.
The spawn count of the smoke source of a vehicle moves out of
smoke_source::update() into smoke::vehicle_spawn_count() (vehicle/smoke.h),
a free function on the mover state, which joins the core library so the
unit tests can call it the way the source does. The rates are the same as
before, the steady rate of the source still counted in before the share of
the engine.
Tests: tests/unit/test_smoke.cpp. A vehicle taken off the tracks spawns no
smoke and drops the pending particles, with the engine starting, idling or
under load; a road vehicle back from the pool puffs again; the engine adds
to the steady rate; the pending count stays within the room of the source.
Two of the four fail without the fix.
Checked again on td.scn with the SM42 driven into a portal: the smoke
source keeps puffing on the run and drops to 0 particles once the vehicle
is taken off the tracks.
The test whether a vehicle belongs to the player train dereferenced
simulation::Train without a null check (commented out long ago). The
driver mode switches to the free camera in the same frame the player train
goes through a portal, before the sounds are updated, so it isn't known to
crash, but nothing else guards it.
The condition added for holding the speed uphill near a stop pushed the
cognitive complexity of control_tractive_force() to 26, over the 25
SonarCloud allows. It now lives in is_too_fast_uphill(); the behaviour
is unchanged.
adjust_desired_speed_for_braking_test runs after the target speed was
worked out and overwrites AccDesired. In its third phase it holds the
deceleration near fAccThreshold until the speed has dropped by 8 km/h.
On a 20 per mille descent that application barely holds the speed, so
the phase can last minutes, and a stop signal coming up meanwhile is
ignored. In one real-time run an E186 with 19 container wagons started
the test 1290 m before a stop signal, lost 7 km/h by the signal and
overran it by 274 m after the emergency application.
While the test runs the AI now uses whichever of the two asks for the
harder braking: the test may brake harder than the route needs, never
softer.
Measured in real time, four runs of 32 consists each on a 20 per mille
descent, on the flat and on the descent with half the rail adhesion:
the test no longer runs within 600 m of a stop signal (before: down to
414 m on this branch, and 14 m with the overrun above in a build with
other brake changes), no overruns, stops 8-15 m short of the signal 86,
117 and 83 of 128 (before 90, 117 and 78).
Within fMaxProximityDist of a stop, going uphill, the AI holds its speed
with AccDesired = min(0, AccPreferred). control_tractive_force only cut
the power uphill when AccDesired < 0, so an AccDesired of exactly 0 kept
the controller where it was. A consist that came into that range under
power kept full power and crept up to the signal.
Holding the speed uphill on the way to a stop now asks for no speeding
up: power comes off once the train is faster than AccDesired allows.
With the current brake valves this rarely shows: on a 10 per mille climb,
four real-time runs of 32 consists each, the E186 with 25 empty 408W drew
power in 0-14 samples of the last 50 m before and 0-8 after, no overruns
either way. It surfaced once the AI predicted the ESt3 brake closer to
what the valve delivers, where the same train crept to 12 km/h from 40 m
out and overran the signal; with this change it stopped short in all runs.
The side sway from the lateral acceleration now goes off only on a track with a sideways value, and the random jolts on one with a vertical or a sideways value. Dips at the rail joints and cross level alone replace neither. On a switch it goes by the path set and what its joined tracks allow.
An edit in the editor also rebuilds the other switches of a joined group with the tracks they meet, and the tracks of a rail run whose joints moved. Removing a track does the same. While dragging, the tracks further off wait until the drag ends.
The clatter of a bogie's axles was right: the followers stand at the pivots and the offsets are the places of the wheel sets. The misleading name goes.
Also what Sonar had left: a parameter as a span, a const method, a struct and its member apart, commented out code, and a function of the grading tool split, with the same output.
* 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.
The blockers are in the EP brake of a vehicle, older than this work: its
arrays made with new[] are vectors now, which also removes the deletes.
- The caches of the unevenness of a track are no longer mutable: what fills
them isn't const any more.
- TSegment gets back to RaPositionGet in one piece, with what the path has
to do with the others in one struct.
- Less nesting in the group of switches and in the fields of the editor,
nested conditionals written out, and the attribute is read from a
string_view. A test no longer counts in floating point.
No change in behaviour.
The rules stay in TrackUneven.h with no dependence on the engine, as they can
be tested. Their use on the tracks moves out of Track.cpp to TrackUneven.cpp,
split into steps: the group of switches, the values of its paths, the meeting
of two ends. One set of helpers for the paths of a track takes the place of
the same conditions written out in several places, and the value at the ends
of a switch is worked out in one place. The clatter of an axle leaves the long
Update of a vehicle. The fields in the editor say themselves when they show.
No change in behaviour.
- A path of no length gave a joint with a vector across of NaN to the path
joined to it, on any scenery, with unevenness or not. It now has no
vector across, and so makes no joint.
- Axles no longer stray along the track. It moved vehicles against each
other, changing the distance their couplers go by.
- The bend under an axle goes straight on past the ends of a path: the
polynomial ran off on a short curved one, with a kick in the cab shake.
- A track left out of its rail run, where tracks are joined one way only,
no longer walks the whole run again on every call.
- The clatter of an axle goes by the outer axle on its side of the vehicle,
and not always by the same one, as the two can be on different tracks.
- A vertex works out the amounts once, and the foot of the ballast not at all.
On such a track the sway drawn from the lateral acceleration, that is
from the curve radius, and the jolts drawn at random are off. Tracks
with no unevenness shake the cab as before. Hunting stays: it goes by the
vehicle and its speed, not by the track.
The phase of the rails was wrapped to the rail length of the track the
vehicle is on, while an axle can be on a track with longer rails. From
then on every frame looked like a joint crossed.
Usage, optional attributes of a track entry, before endtrack:
uneven <mm> peak deviation of a rail on a 10 m chord, 0-100; normal and switch
unevenup <mm> up and down alone, instead of uneven; normal and switch
unevenside <mm> sideways alone, instead of uneven; normal and switch
unevencross <mm> cross level alone, instead of uneven; normal only
jointdip <mm> depth of the dip at rail joints, every sound distance; normal only,
default 0.4 of the up and down component
Values of uneven follow PLK Id-14 appendix 1: 6 mm is the limit at 160 km/h, 10 at 120,
18 at 80, 35 at 40; half of it for a track in a good state. Example:
uneven 18
unevencross 25
jointdip 6
endtrack
The same fields are in the editor, track panel, section Ride and sound. See
track_uneven_doc.md.
The unevenness is a displacement field over the plan of the scenery, with no random
numbers: one place strays the same every time, and joined tracks, the paths of a switch,
sleepermodel and axles at one place move together. Tracks meet at a joint at the lower of
their values. Joined switches take the lowest of theirs, the tracks joined to them come to
it, and a track with none of a component keeps the switch to its design in it. The clatter
comes from the dipping joints. Curve radius and grade for the physics stay as designed.
Tracks with it get geometry every 2 m on straights: 4x the vertices on Baltyk with all
tracks flagged, 0.2 ms more per frame on td.scn.
Plays internet radio streams (http, mp3) through a positional emitter in the cab, with analog tuning, inter-station noise and two speaker profiles. Stations are listed in fmradio.txt, cab controls are defined with fmradio entries in the .mmd file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ai): release what the AI was holding when the player takes over
* fix(ai): keep the light selector lookup within its table, Sonar findings of the handover
- the light selector size comes from the vehicle definition (LightsList Size)
and isn't limited, while the table holds 18 positions; a bigger selector
read past the table, and so did a table index other than the front or
rear one. Only the positions the table holds are looked at now, and the
selector stays where it is for a bad table index (cpp:S2259),
- the lambdas of the handover capture what they use explicitly (cpp:S3608),
- the chained assignments resetting the headlight state on the AI taking
over are split (cpp:S1121).