17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-09 17:12:46 +01:00
Commit Graph

3417 Commits

Author SHA1 Message Date
dad5a32f36 Merge pull request #227 from dos1/turbidity
simulationenvironment: Adjust turbidity values
2026-11-07 20:26:39 +00:00
15e6123ca4 Merge pull request #228 from dos1/xyz
utilities: XYZtoRGB: Use sRGB colorspace
2026-11-07 20:25:49 +00:00
4cce47aa05 Merge pull request #230 from dos1/sunsize
opengl33renderer: Fix up the sizes of Sun and Moon
2026-11-07 20:24:39 +00:00
dccb26b77a Merge pull request #234 from dos1/tristate
Train: Handle 3-way light switches in headlighttoggle actions
2026-11-07 20:24:19 +00:00
cd52ec8799 Merge pull request #259 from MaSzyna-EU07/fix/sm03-engine-shutdown
fix(cab): a diesel with only the starter button stops its engine with the main switch off command (eu07/maszyna#427)
2026-11-07 20:23:21 +00:00
d6f20f1b0f Merge pull request #260 from MaSzyna-EU07/fix/sx45-line-contactors-lamp
fix(cab): on the SU45 and SU46 the open line contactors lamp lights with the contactors opened by the pressure switch (eu07/maszyna#263)
2026-11-07 20:22:53 +00:00
3ea5788139 Merge pull request #262 from pwarszawski/fix/ai-releaser-standstill
AI: use the locomotive releaser only when starting off
2026-11-07 20:22:35 +00:00
eb3dc3f8f8 Merge pull request #264 from pwarszawski/feat/ai-braking-earlier-steps
AI braking: smaller steps, earlier, while approaching a stop or a lower speed
2026-11-07 20:19:33 +00:00
pwarszawski
04b208bbaf Merge branch 'fix/est3-valve' into feat/ai-braking-earlier-steps 2026-11-07 21:17:12 +01:00
pwarszawski
39244a88a5 tests: wait for the pressure switches before notching up
full_power() notched up as soon as the controlling half had closed its line
contactors. In the ET42 test the locomotive brake is still at about 2.0 bar
then, right at the pressure switch threshold, so whether the second half
starts as well depended on thousandths of a bar in its cylinder: a half whose
switch is on when the controller leaves zero doesn't close its line
contactors until the controller is back at zero. Any change to the air
brakes could tip it either way (with the corrected ESt3 the second half
stayed off and the train reached 88.7 instead of 106 km/h).

The helper now waits, as a driver does, until no pressure switch in the
train cuts the start off.
2026-11-07 21:17:12 +01:00
pwarszawski
71d4645e09 Merge remote-tracking branch 'origin/master' into fix/est3-valve 2026-11-07 21:17:04 +01:00
pwarszawski
8d1d9eec9b AI braking: smaller steps, earlier, while approaching a stop or a lower speed
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.
2026-11-07 18:10:47 +01:00
maj00r
939e42b611 docs: CLAUDE.md with the team's notes for Claude Code (#258)
* docs: CLAUDE.md with the team's notes for Claude Code

The rules Claude Code sessions kept relearning, shared with the team:
- the code builds and behaves the same with GCC 13 (Linux CI and the
  Sonar build) as with MSVC, with the MSVC leniencies to watch for;
- every SonarCloud remark on the changed code gets fixed without
  changing behaviour, with the recipes for the rules that keep coming
  up;
- building, the unit tests, scripted editor runs, the game data and
  the profile folders, and GPU hangs;
- the game data in the asset SVN and backward compatibility: existing
  files keep being read as before, new entries are optional, code
  falls back without new assets and a change that uses them says so
  in its commit message, what the editor saves stays readable by the
  current release, new features can be turned off in eu07.ini and
  don't change the look of existing data abruptly; StarterNG is the
  official starter.

* docs: CLAUDE.md on the move to OpenGL 4.6 and Vulkan

betterRenderer/ is to be removed and the OpenGL 3.3 and legacy
renderers are being replaced by feat/gl46-migration (PR #216, a draft
with milestone 27.04). Work on that code and on the shaders is kept to
fixes and to what a feature on master needs to be drawn, with the PR
saying where the branch has the counterpart.

---------

Co-authored-by: maj00r <maj00r@host>
2026-11-07 16:17:57 +01:00
pwarszawski
fddeea8b0e AI: use the locomotive releaser only when starting off
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.
2026-11-07 15:49:46 +01:00
pwarszawski
439e892abd AI: use the locomotive releaser only when starting off
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.
2026-11-07 15:35:35 +01:00
917c472cc9 Merge pull request #257 from MaSzyna-EU07/fix/vehicle-removal-smoke
fix(gfx): a vehicle taken off the tracks stops smoking (#90)
2026-11-07 13:28:31 +00:00
jerrrrycho
d90edb6980 fix(cab): on the SU45 and SU46 the open line contactors lamp lights with the contactors opened by the pressure switch (eu07/maszyna#263)
On the electric locomotives the lamp of the open line contactors tells the start can be made, so it goes out with the
start cut off by the pressure switch. The diesel-electric SU45 and SU46 (the only diesel-electrics with the lamp) use
it just to show the contactors are open, but it went out the same way: with too low a pressure in the main pipe the
contactors were open and the lamp out. Once the pressure came back with the master controller past the first
position, the contactors stayed open, as they should, and the lamp lit up on a power position, which looked like
the lamp working the other way round.

On the diesel-electrics TMoverParameters::LineContactorsLampLit() now follows the contactors alone; the electric
locomotives and the other main circuit lamps are unchanged.

Tests: tests/unit/test_line_contactors.cpp, on a locomotive made after the SU46.
build-fix-sx45-line-contactors-lamp
2026-11-07 14:06:58 +01:00
jerrrrycho
7e426192b7 fix(cab): a diesel with only the starter button stops its engine with the main switch off command (eu07/maszyna#427)
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.
build-fix-sm03-engine-shutdown
2026-11-07 14:06:38 +01:00
Sebastian Krzyszkowiak
4a256477e0 Train: Handle 3-way light switches in headlighttoggle actions
This lets them be fully operated with mouse and VR pointers.
2026-11-07 13:37:27 +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
pawelw
bb009b0499 Text displays: draw opaque characters in the opaque pass (#201)
* Text displays: accept characters above 127, in windows-1250 or utf-8

A text display (sub-model of type "text") looks up the sub-model of each character
in a 256 entry table, and indexed it with a plain char. With char being signed, any
character above 127 gave a negative index: reading the text went outside the table,
and so did filling the table if a character sub-model had such a name. In effect the
displays could only show ASCII, and a text with national characters could crash.

The lookup is now keyed by unicode code point. Both the displayed text and the names
of the character sub-models are decoded as utf-8 if they are valid utf-8, and as
windows-1250 otherwise, so legacy (ansi) sceneries and models work next to utf-8
ones, and the two can be mixed. A short ansi text can be valid utf-8 by accident
(two upper case accented letters in a row will do); when both readings are possible
the one which finds more of its characters in the model is used.

The rest is unchanged: the first character of the text still isn't displayed, the
characters are still drawn in the translucent pass only. A missing text pointer no
longer gets dereferenced.

* Text displays: draw opaque characters in the opaque pass

The characters of a text display were only laid out in the translucent pass, so a
display had to be made of translucent sub-models to show anything. Those don't write
depth: overhead wires and other translucent geometry drawn later showed through the
display, and translucent geometry in front of it could end up painted over.

The opaque pass now lays the characters out the same way, so a display built from
opaque sub-models is drawn with depth like any other opaque geometry (and casts
shadows). Translucent characters keep working as before.

A model with a text display shows the text of its own instance, so such models are
excluded from the GPU-instanced opaque path.

* Text displays: prepare the characters once for each text

The sequence of character sub-models was cached for the last text only, so
displays of several instances drawn in turn decoded their texts again on
every draw. It is now kept for each text the display shows; the texts of the
instances don't change, so a draw costs a single lookup.

Also the comments added in the renderers are now in English.

* Text displays: move the text decoding to utilities

utf8_to_utf32() and win1250_to_utf32(), with the windows-1250 table, now sit
next to win1250_to_utf8(). The existing function maps only the Polish letters
and passes other bytes above 127 through, so it can't serve the displays.

* Text displays: address the SonarCloud findings

std::array instead of the C-style arrays, the utf-8 sequence length worked
out in a function of its own instead of nested conditional operators,
std::byte for the bit operations, std::ranges::count_if and contains().
The character loop of the renderers moved to Render_Alpha_text(), which
takes the nesting back to the limit, and the static pasText is accessed
through the class.

* Text displays: move the opaque character loop to Render_text()

The same as for the translucent pass: the loop sits in a function of its
own and the static pasText is accessed through the class, which keeps the
nesting within the limit.

* Text displays: access the static iAlpha through the class in the opaque pass

* Text displays: transparent lookup of the text layouts, init-statement for the lookup result

Two SonarCloud findings left in the code of #198: the map of the text layouts
gets a transparent hasher with std::equal_to<>, and the result of the lookup
is declared in the if statement.
2026-11-07 12:36:17 +01:00
pawelw
55ba7a9f41 Instancing: skip the buckets of a cell lying wholly beyond the range of their instances (#225)
* perf(instancing): skip a cell's buckets lying wholly beyond the range of their instances

* perf(instancing): go only through the instances drawn one by one in the per-node pass

* perf(instancing): leave out of the color pass the cells holding only instances too far to be drawn

* refactor(instancing): keep the caches of the instances of a cell without mutable fields, set by the renderer when the cell is queued
2026-11-07 12:35:55 +01:00
Sebastian Krzyszkowiak
3a29a165a9 simulationenvironment: Don't multiply fog color by diffuselevel (#229)
It can easily make the fog too dark compared to the environment,
producing a "fire in the forest" effect.
2026-11-07 12:34:01 +01:00
Sebastian Krzyszkowiak
605a94cf2b uilayer: Force ui viewports to be disabled under gamescope and Steam Big Picture (#232) 2026-11-07 12:33:46 +01:00
Sebastian Krzyszkowiak
ecdef7f753 opengl*renderer: Show an empty frame early (#233)
Gives a better feedback to the user that something has actually launched.
2026-11-07 12:33:22 +01:00
maj00r
088eeaa743 fix(editor): a scenery saved under another name keeps one file of turntable controls, not the old one beside its own (#251)
The editor writes the turntable controls on each save to <scenery>_obrotnice.scm and includes it. A copy of a
scenery, saved under another name, still included the file of the scenery it came from. Its save wrote a file of
its own and included it too, so each turntable had its events twice.

Now the save leaves out of the scenery the other files of turntable controls it includes, told by the line the
editor writes at their start. Their files stay on the disk, for the scenery they were made for. A file of
turntables made by hand, without that line, stays included.

Co-authored-by: maj00r <maj00r@host>
2026-11-07 12:32:02 +01:00
349e94aef8 Merge pull request #245 from MaSzyna-EU07/split/2-materials
Split/2 materials
2026-11-07 11:20:50 +00:00
bcda4aaa42 Merge pull request #244 from MaSzyna-EU07/split/1-small-fixes
Split/1 small fixes
2026-11-07 11:19:45 +00:00
3f6a10f1fe Merge pull request #237 from MaSzyna-EU07/feat/track-uneven
Feat/track uneven
2026-11-07 11:18:49 +00:00
jerrrrycho
4489dd7469 test(gfx): unit tests of the smoke of a vehicle taken off the tracks (#90)
The spawn count of the smoke source of a vehicle moves out of
smoke_source::update() into smoke::vehicle_spawn_count() (vehicle/smoke.h),
a free function on the mover state, which joins the core library so the
unit tests can call it the way the source does. The rates are the same as
before, the steady rate of the source still counted in before the share of
the engine.

Tests: tests/unit/test_smoke.cpp. A vehicle taken off the tracks spawns no
smoke and drops the pending particles, with the engine starting, idling or
under load; a road vehicle back from the pool puffs again; the engine adds
to the steady rate; the pending count stays within the room of the source.
Two of the four fail without the fix.

Checked again on td.scn with the SM42 driven into a portal: the smoke
source keeps puffing on the run and drops to 0 particles once the vehicle
is taken off the tracks.
2026-11-07 11:38:49 +01:00
jerrrrycho
f94b308062 fix(sound): check for the player train in the outer noise test of a vehicle
The test whether a vehicle belongs to the player train dereferenced
simulation::Train without a null check (commented out long ago). The
driver mode switches to the free camera in the same frame the player train
goes through a portal, before the sounds are updated, so it isn't known to
crash, but nothing else guards it.
2026-11-07 09:37:40 +01:00
jerrrrycho
28cf5fe276 fix(gfx): a vehicle taken off the tracks stops smoking (#90)
The smoke sources of a vehicle stay in the particle manager when the vehicle
is taken off the tracks: one gone through a portal, or a road vehicle
waiting in the pool of its spawn point. They kept spawning particles where
the vehicle had vanished, for good. A source of a disabled vehicle spawns
no more particles now, the ones it let out fade away as usual, and a pooled
road vehicle puffs again once it's back on the road.

Checked on td.scn with a portal added at the dead end of tdo_n585 and an
SM42 driven into it by the AI: on master its smoke source kept about 100
particles where it vanished for the rest of the run (over 200 s); with the
fix they were gone within a second.
2026-11-07 09:37:40 +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
maj00r
d232cd0a69 refactor: the Sonar remarks on the material copies taken care of
- the params of a shader named by the type the shader keeps them in
- ranges::any_of and erase_if, with explicit captures
- the weathering set component by component with an index of the
  type of the vector
- the lookups declared in the ifs that use them
- the empty Erase of the base renderer told why it is empty
2026-11-06 19:58:24 +01:00
maj00r
a6778c9fe2 refactor: the Sonar remarks on the small fixes taken care of
- the log stops through a flag kept in a function, not a global one
- the messages are put together with std::format
- a vehicle dropped on a trainset in the launcher goes in once the
  trainset is drawn, so the vehicles are gone through with a plain loop
  again; the drop split into a new vehicle and a moved one
- the scenario that can't be opened is caught as the runtime error it
  is, a file name outside of the code page as the system error it is
- a track of an unknown type is left out through the pointer owning it
- the keyword of a track type written out by a function of its own
- the picked node forgotten with ranges::replace_if, the empty
  Forget_Node of the base renderer told why it is empty
- FileCased takes the path it works on
2026-11-06 19:54:54 +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
maj00r
09b1c2de10 fix(utilities): win1250_to_utf8 turns the polish letters into utf-8, not into themselves (#252)
utilities.cpp starts with a byte order mark, so the compiler reads it as utf-8. The game's targets don't get
/utf-8, though: set_target_properties(... COMPILE_OPTIONS ...) in the MSVC block of CMakeLists.txt replaces the
options add_compile_options gave them. So the string literals went into the program in the code page of the
system, windows-1250 on a polish one, and the map of win1250_to_utf8 turned each letter into itself.

What read windows-1250 through it got no utf-8: the names, descriptions and categories of the sceneries and the
names of the textures in the launcher, and the descriptions of the templates in the editor. The editor writes
such a description back through utf8_to_win1250, which makes a question mark of each byte that doesn't start
a utf-8 sequence, so the letters it had read unchanged didn't come back.

The utf-8 sequences are written as bytes now, the way utf8_to_win1250 has them, so they don't depend on the
options. The options stay as they are: with /utf-8 other literals of the files with a byte order mark would
change, the "±" of the table of Bezogonkow would take two bytes and move the letters after it.

The unit tests check the letters and that the text given is valid utf-8.

Co-authored-by: maj00r <maj00r@host>
2026-11-05 03:12:05 +01:00
maj00r
b7ff8bab73 Merge pull request #236 from MaSzyna-EU07/fix/imgui-panels-no-auto-detach
fix(ui): keep panels in the main window when resizing them at the scr…
2026-11-04 01:08:30 +01:00
maj00r
8523316006 fix(track): a tributary loads as a river of its first path keeping the second, a track of an unknown type is left out, where both made the simulator fall over as it loaded
On master neither got a segment: update_location() dereferenced the missing one of a track of an unknown type, and
a tributary went to Switch(0), which logged "Cannot switch normal track", with no segment under it either.

A tributary is now a river of its first path. Its second path is kept in m_paths, isn't drawn nor run on, and is
written back with the type "tributary". A track of an unknown type is logged and left out by the loader; as it is no
node, its text stays in the file when the editor saves the scenery.
2026-11-03 23:06:34 +01:00
maj00r
48af69cbf1 fix(render): the matte and tinted copies of a material made again with its season and weather, the primitive of a handle to no chunk told as none
The copies don't update themselves, their names aren't files to read again: after a change of the weather or the season the material got its new textures and its copies kept the old ones. They are made again from it then, under their handles. primitive() of an empty handle or one past the banks read out of the bank list; it gives no_primitive there, -1 from the renderer.
2026-11-03 22:00:48 +01:00
maj00r
ed0ff36a69 fix(app): a scenario failing as it loads goes back to the launcher too, the files of other cases looked for only off Windows
A scenario failing past its opening popped the loader mode and went on to pop it again and push the driver mode with the scenery half loaded; it goes back to the launcher as one that can't be opened. FileCased() goes through the folder only where the file system tells the case apart: on Windows a missing file was looked for in its whole folder for nothing, and a name outside the code page threw from the parser. The Discord thread sleeps a quarter of a second outside the driver mode, not to hold the closing up.
2026-11-03 21:53:52 +01:00
maj00r
89c4555e1e feat(render): matte and tinted copies of a material, the steel weathered or tinted as far as the userdata of its vertices says
Matte_Material() gives a copy of a material with its glossiness damped and its weathering on: the faces cut out of a polished surface, as the sides of a worn rail head, darken and lose the shine by the userdata of their vertices. Tinted_Material() gives a copy whose texture is tinted to a colour as far instead. Both are made once per material.

The weathering and the tint colour have a param slot of their own, param[3]: the shaders of the materials use all four components of param[2] among them (detail, parallax, water, the terrain placement), and light_common.glsl is included in all of them. MAX_PARAMS grows from 3 to 4, the model UBO by 16 bytes. Neither copy is made when another param of the shader lies on the components it would set.
2026-11-03 21:51:05 +01:00
maj00r
a24118baf7 feat(render): a geometry chunk emptied in place, the kind of its primitive told; a chunk gaining or losing its userdata gets a new buffer
Erase() empties a chunk and keeps its handle to be filled again, for geometry made anew as it changes. Primitive() tells whether a chunk holds triangles or strips, -1 from a renderer which doesn't know. The VAO bank reused the buffer of a chunk of the same length even when the userdata came or went, the layout of the buffer differing then.
2026-11-03 21:50:31 +01:00
maj00r
e230d7b902 fix: a deleted model is no longer the picked node, an orthophoto tile isn't swept as a grey stub, the ED brake arrays freed
Deleting a model in the editor left the renderer holding it as the picked node, the next pick used freed memory: the renderer forgets it now (Forget_Node). A texture filled straight from memory (internalsrc:) can't be read again, released by the garbage sweep it came back grey; it stays now, and the units bound with the old name are rebound. The arrays of the ED brake force sharing are vectors, they leaked once per update. The scene hierarchy and the editor menu items go through the translation. Python caches ignored.
2026-11-03 21:45:58 +01:00
maj00r
0d69059448 fix(launcher): the scans of the sceneries and the vehicles go past folders they can't read, a vehicle dropped without its skin set isn't added
A folder without the permission to read it or a name that can't be converted ended the scan with an exception. Ctrl+Delete with no trainset selected, a dropped skin whose vehicle is gone, and a description with a % in it no longer crash the launcher.
2026-11-03 21:45:58 +01:00
maj00r
e75d849679 fix(app): a scenery file named in another case than given is found, a scenario that can't be opened goes back to the launcher
The command line gives the name of the scenery lowercased, and the sceneries made on Windows name their files and includes in any case: on a case-sensitive filesystem they weren't found. FileCased() finds the file whose name differs only in case; the parser and the scenario loader use it. A scenario that can't be opened logs the reason and goes back to the launcher instead of ending with an exception.
2026-11-03 21:45:57 +01:00
maj00r
b34238f33f fix(app): the log written to the end of the closing, each step of it with its time; the Discord thread idle outside the driver mode
The log service stopped with applicationQuitOrder, so whatever was logged while the simulator closed was lost; it now runs until StopLogService() and writes out what came before. The closing logs each of its steps with the time since it began, to tell where a closing that hangs stops. Outside the driver mode the Discord thread spun a core in its loop, it now sleeps a second between the checks.
2026-11-03 21:45:57 +01:00
pwarszawski
6374e2d32e AI braking: predict the ESt3 brake force from the distributor characteristic 2026-11-02 20:07:32 +01:00
pwarszawski
bb43f559e8 ESt3: spring preload, minimum stage, releaser time and AL2 timing
Corrections to the TNESt3 distributor, checked against the ESt3 data
and acceptance requirements (orange book tables 3-5, 3-7 and 3-9) and
discussed with youBy:

- The main control device settles at (control reservoir - pipe - 0.21)
  times the ratio instead of using a 0.05 bar offset. The 0.21 bar is the
  spring preload; without it the first stage came 0.2 bar of pipe drop
  too early. BVM is derived from the same offset, so a full service drop
  still gives MaxBP.
- When the valve brakes it gives at least the minimum stage of 0.28 bar
  (acceptance tests 12 and 22 require 0.25-0.30 bar); when the target
  drops to zero the cylinder vents completely.
- The automatic releaser vents the 15 l control reservoir from 5.0 to
  0.4 bar in about 23 s (test 27: 23 +/- 3 s) instead of 5.6 s.
- 12" cylinder fill nozzle dTN is 1.62, not 1.65 (typo).
- With the AL2 relay the impulse chamber is 1 l and the cylinder nozzles
  are scaled so that the fill and release times meet table 3-9; before,
  the AL2 timings had been set by eye.

On a valve test bench running the acceptance procedures, the number of
results out of tolerance drops from 77 to 40. The remaining ones need
parts that are not modelled yet (accelerator venting, ZP/ZS filling,
intermediate chamber).
2026-11-02 20:07:32 +01:00
pwarszawski
18bc7e06bf AI braking: move the uphill power-off check into its own function
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.
2026-11-02 16:43:16 +01:00