News:

Congratulations!
 You've won the News Item Lottery! Your prize? Reading this news item! :)

Show a line's real route on the main map

Started by victor_18993, July 23, 2026, 09:56:02 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

victor_18993

Right now Simutrans tells you a line's stops, but not the path its vehicles actually take between them.
On a simple network you can guess it; on a dense one with parallel tracks, one-way sections and waypoints,
you often can't. I would like to add an optional overlay that draws the real route of a selected line on the
main isometric map, so you can see where it goes, in what order it visits its stops, where the two directions
differ, and where a segment has no valid route at all.

I want to be honest about what already exists, because this proposal is deliberately built on top of it
rather than reinventing anything:

- The minimap already draws line routes, but as straight stop-to-stop segments in 2D, not the real path over
  the rails, and not on the main view.
- The schedule editor already highlights a schedule's stop tiles on the main 3D map (visualize_schedule),
  reusing the existing highlight and marked flags. It highlights the stops, not the path between them.

So what I am proposing is essentially the schedule highlight applied to a whole line's real route: same
proven rendering mechanism (the existing per-object highlight and per-tile marked flags, cleared when the
window closes), but the tiles that get highlighted are the ones the engine's own pathfinder returns, not
straight lines. The route would be computed with the real routing code (route_t::calc_route), segment by
segment between consecutive schedule entries, so it follows real track, respects one-way masks and signals,
and can honestly show a segment as broken when there is no route.

One real design question I would like your opinion on. A route in Simutrans is not a property of the line, it
is a property of a vehicle: electrification, minimum-speed signals and one-way rules can make the same
schedule route differently for different convoys, and a line can have convoys that differ. For a first
version I would compute the route of the line's first active convoy and label it clearly as such, and for a
line with no convoys fall back to straight dotted stop-to-stop segments marked as approximate. I think that
is the honest minimum, but I would rather hear your view than guess.

What I am NOT proposing, to keep the scope small and safe:

- no savegame format change, and no persistent state of any kind
- no change to how convoys move or how routes are calculated
- no new overlay architecture and no new classes; it reuses the highlight/marked flags
- the route is computed once when you turn the view on (never per frame, since the pathfinder cannot run
  inside the display step), cached, and cleared when you close the window or switch line
- one line at a time, opt-in from a single button on the line window, off by default
- it stays purely local and visual, so it is safe in network games

Direction arrows on the main map, distinguishing overlapping there-and-back on the same track, and later
diagnostics (where electrification ends, a platform too short, a blocked segment) are natural follow-ups, but
I deliberately left them out of the first version.

Before I write any code I would like to know: do you want this real-path-on-main-map view at all, given the
minimap and schedule-highlight precedents? And does computing the first active convoy's route sound like the
right representative choice, or would you frame it differently? I would rather agree the shape first and then
send a small, focused patch.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

prissi

Some things to consider:
1) Routes cannot caluclate in the GUI code, since it runs in sync_steps while route calculation can only run in a step (since there is only on static mapping and bucket list). Hence, one would need an internal tool that highlight the route as those are run in a step. Certainly doable, adds some overhead.
2) Also, some routes, like ships can be very long and take longer to computer. Also water tiles do not highlight as they have no way. Same for airplanes. (Also routes change on runway utilisation there) and there are routes which are not on ways too.
3) I would suggest a second highlighting color for way in that case, so one can see the stop. Otherwise the stop cannot be change easily.



victor_18993

Thanks, this gives me a much clearer direction.

I agree that the route calculation should not live in the GUI path. I will rework the prototype so that the GUI only requests the operation, while an internal tool performs the route calculation during a proper step and then stores the result for display.

I will also separate the route overlay from the stop highlighting instead of trying to reuse the same highlight state. A second colour should make the route and its scheduled stops much easier to distinguish and avoid the current interaction between both highlights.

For water, aircraft and route sections without a way, I will not assume that every route tile can be highlighted as infrastructure. I will first define how those unsupported or non-way sections should be represented, and I will include long ship routes in the performance tests before extending the patch.

I will keep the current version as a proof of concept and make these architectural changes in the next iteration. Thanks for pointing out the step/sync-step distinction and the cases that the current prototype does not cover.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

victor_18993

Hi prissi, and everyone following the thread,

Thank you for the detailed feedback earlier. I took the direction you gave and built a first functional slice around it, rather than keep piling onto the prototype. I would really like your read on the architecture and the visual approach before I extend the scope any further.

This is a first cut, not a finished solution. It is meant to be reviewed.

What changes

The route no longer borrows the obj_t::highlight bit. That bit is now left entirely to the stops. The route geometry is kept in a separate, display-only overlay, and the stops keep their red highlight through the existing mechanism. So route and stops are now two independent channels: a tile can be on the route without being flagged, and a stop stays flagged whether or not the route passes through it.

The route is drawn in the owning player's colour, so different companies' lines read differently. Outward and return legs are shown on opposite lanes, so a there-and-back line reads as two parallel lines instead of one line drawn twice. That directional offset reuses the engine's existing driveleft offset table, with the travel direction taken from ribi_t::get_dir, so the return leg lands on the opposite side by construction.

How it fits the architecture

I tried hard to stay inside how Simutrans already works rather than bolt on a parallel graphics system.

The route is no longer computed from the GUI or in sync_step. Toggling the overlay from the line window only issues a small local tool; the actual pathfinding runs later, inside a normal tool step. The world holds the resulting path as temporary visual state, and main_view_t::display draws it procedurally. Nothing is serialized, nothing touches routing, the schedule or determinism, and the overlay is cleared on new map, load and rotate90.

Visual finish

The stroke is a player-colour core with a discreet dark outline and slightly rounded joins and ends, so it reads on both dark and light ground without hiding the way underneath. The exact weight, halo and brightness are a first, conservative proposal — very much open to your taste.

Validation

Rebased onto r12102. Clean build, and the product patch also builds in isolation. The small dedicated bench passes 8/8 (including the check that route and stop channels stay independent), the standard test suite passes 201/201, and a determinism run comes out identical. I checked the visuals on dark and on snow-covered (light) terrain, on straights, diagonals, a loop, a there-and-back, and at near and far zoom.

Current scope

This first slice deliberately does not include ships, aircraft, or route sections with no way under them yet. Automatic invalidation after a schedule edit is also not wired up, so for now the overlay reflects the route as of the last time it was toggled on, and there is no distinct marker for broken legs. I did not want to guess at those before the core representation is settled.

One honest note on left-hand driving: the lane offset reuses the engine's own table and should be correct by construction, but a clean visual check on a map created with left-hand traffic from the start is still pending. I would rather flag that than claim it is verified.

What I would appreciate your view on

- whether keeping the route in a separate display-only overlay fits the architecture you have in mind;
- whether the small accessor I added on vehicle_base_t (to reuse the lane offset table) is acceptable, or whether you would prefer it exposed differently;
- whether the drawing belongs in main_view_t::display or somewhere else;
- whether the visual finish is going in the right direction;
- and whether it is worth continuing next with no-way sections and schedule-edit invalidation.

I will hold off on extending the scope until I hear what you think. Thanks again for the guidance.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

victor_18993

First reviewable implementation: showing a line's real route on the main map
Thanks for the earlier feedback and for pointing me towards using the real route and the existing directional offsets.
I have now prepared a first reviewable implementation following that direction and keeping it inside the current Simutrans architecture.
The main change is that the route no longer reuses
obj_t::highlight.
Instead, the calculated path is stored as a temporary display-only overlay in the world. The stops keep their existing red highlight, so the route and the stops now use two independent visual channels.
The GUI does not calculate or own the route. The button only activates a tool, the tool calculates the real route, the world stores the temporary overlay state, and
main_view_t::display draws it.
The route:
  • uses the player colour;
  • follows the actual calculated tiles;
  • places outbound and return directions on opposite sides;
  • reuses the existing driving-side offset table;
  • resolves directional offsets at shared nodes, so the lines remain continuous through corners instead of crossing;
  • uses a small bevel at normal turns and a compact return connection for 180-degree reversals.
I also added a conservative visual treatment: a two-pixel player-coloured core, a thin dark outline and small rounded joins. This is only a first visual proposal and I am completely open to changing the thickness, brightness or outline if another style would fit Simutrans better.
The overlay is not saved and does not affect routing, simulation state or determinism. It is cleared together with its stop highlights and active line identity when the map is initialised, loaded or rotated.
Failed route sections are now separated explicitly, so the renderer cannot accidentally connect two valid sections across an unroutable gap.
The line-management buttons also read the active overlay state from the world. This avoids inconsistent states when several line windows are open: switching from one line to another updates the real active line, and closing an unrelated window does not remove the currently displayed route.
The current patch is based on
r12102 and contains only the ten product files. The test scaffolding is separate and is not included in the attached patch.
Validation completed:
  • isolated Release build of the product patch on a clean
    r12102;
  • no new warnings in the modified product files;
  • dedicated route-overlay bench: 10/10;
  • standard suite: 201/201;
  • deterministic repeated runs;
  • visual checks on dark and light terrain, loops, diagonals, reversals and long schedules.
The current scope is intentionally limited. It does not yet include:
  • ships;
  • aircraft;
  • routes without a way;
  • automatic invalidation after editing a schedule;
  • a special marker for broken route sections;
  • more than one route overlay at a time.
Before extending it further, I would appreciate feedback on the architecture, especially:
  • whether keeping the temporary overlay state in
    karte_t is acceptable;
  • whether drawing it from
    main_view_t::display is the right place;
  • whether the small public accessor for the existing driving-side offsets is acceptable;
  • whether the node-based joins and the proposed visual style are going in the right direction;
  • and whether the next step should be routes without a way or automatic invalidation after schedule changes.
I am attaching the product patch and a few screenshots showing the independent route and stop highlights, directional lanes, corners and the dark/light terrain comparison.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

prissi

Impressive, but some comments:

It should work with normal schedules too, not only lines, and better automatically and update when a schedule updates, like the current stop highliting. I know that is some extra work. To make it easier, after each schedule change tool is doing its ::work, it could check whether the displayed schedule is the current and update this.

The state machine of some vehicles is changed after calcualting a route (convois after choose signals or overtaking, and airplanes totally). So you cannot use the front vehicle as test_driver since its state would be different afterwards but need to allocate a new one.

Better treat all curves as diagonals as routes going diagonal stretches look like staircases. For this, the route also can return a ribi, which would indicate the required direction. This would probably solve also the current "hanging corner" problem.

victor_18993

Thank you, these comments are very helpful.

I agree that the overlay should not be limited to lines. I will generalise it so that it can display both line schedules and standalone convoy schedules. I will also make it update automatically after a successful schedule change, but only when the modified schedule is the one currently being displayed.

I will replace the current use of the convoy's front vehicle as the test driver. The revised implementation will create a separate temporary vehicle based on the representative vehicle descriptor, so route probing cannot alter the state of a real convoy. I will pay particular attention to choose signals, overtaking and the specialised aircraft route calculation.

For the rendering, I will preserve the ribi returned by the calculated route instead of reconstructing the direction only from adjacent coordinates. Curves will then be rendered as diagonal stretches where appropriate, which should also address the staircase and hanging-corner problems.

These changes require some restructuring of the prototype, especially changing the stored overlay identity from line-only to a general schedule source. I will prepare a revised patch and regression tests before posting the next version.

Thanks again for pointing me in the right direction.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

prissi

Do not worry about chose signals. That routes may diverge is ok, that is their purpose. Just use the return of test driver. Make sure, that the test driver has the same owner, as route with private signs may otherwise differ.

victor_18993

Hello prissi,

I have reduced and revised the proposal following your feedback.

The feature now works for both sources:

- a line schedule, from the line-management window;
- an individual convoy schedule, from the convoy-information window.

Both use a small "Show route on map" button. The route is calculated with a disposable vehicle probe of the correct subclass and drawn as a display-only overlay. It does not modify the real convoy, reservations, savegames or synchronized network state.

The revised implementation also:

- keeps the route ribi, so diagonal stretches are drawn as straight diagonals instead of a staircase;
- highlights scheduled stops through the existing, separate highlight channel;
- updates automatically after line, convoy or individual-schedule changes;
- follows a convoy requester when it changes line or becomes lineless;
- reacts centrally when representative convoy membership changes, including depot sales, line deletion, automatic trimming and direct AI/internal calls;
- chooses another usable convoy when the representative disappears, or clears the overlay when none remains.

The route calculation preserves the relevant convoy context, including ownership, electrification, speed and convoy length. The maximum platform-search length matches the vehicle family: road uses convoy length, rail uses convoy length or 8888 according to stop_halt_as_scheduled, and water uses zero. Aircraft keep their existing air_vehicle_t routing path.

Validation:

- clean MinGW64 build;
- automated deterministic bench: 24 PASS, 1 SKIP, 0 FAIL;
- road, rail, monorail and ship validated end-to-end;
- exact overlay trace compared with an independent, non-destructive oracle;
- requester, deletion, line-change, broken-leg and diagonal-route regressions covered;
- visual validation performed in-game.

The single SKIP is the aircraft end-to-end scenario: pak64 rejects scripted airport-stop placement on the flat headless fixture. The aircraft code path is selected, but I am not claiming a complete aircraft test.

The attached patch contains only the 15 product files and is based on r12103 (315c9cf66). The test-only scripting hooks and temporary screenshot tour are not included.

I would appreciate feedback on whether this reduced architecture is suitable for upstream, particularly:

1. whether the transient overlay state belongs on karte_t;
2. whether the lifecycle notifications from convoi_t/simline_t are acceptable;
3. whether the same button in line and convoy windows is the desired UI.

Regards
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

prissi

The halt overlay is activated and deactivated with the schedule entry gui. I think the route overlay should be automatic like the stop highlighting, then it would also vanish like the stop highlighting, no notification needed.

A line without covnois could just drive a generic vehicle for the route.

victor_18993

Thanks - that was the right call. I have rewritten it around visualize_schedule instead of
around its own life cycle, and almost everything that was there before turned out to be
scaffolding for a problem that no longer exists.

What is gone: the "Show route on map" buttons in line_management_gui and in the convoy window,
the persistent manual on/off state, the global requester that identified which window had asked,
the tracking of a representative convoy and its replacement when sold, and the notifications out
of convoi_t and simline_t. No dead code from any of it is left. The savegame format is untouched
and there is no new translation key.

The route now hangs off schedule_gui_stats_t::highlight_schedule, the same single function that
already switches the red stop highlight. Everything already passes through it - opening,
editing, the tab switch in line_management_gui, closing, the destructor, and the
visualize_schedule setting - so there is nothing new to keep in sync. When it marks, it asks the
world for a route; when it unmarks, it drops it.

The calculation still runs in a safe context. I put it directly below your next_deferred_move_to
block in karte_t::interactive(), for the same reason your comment there gives - outside
sync_step, so the non-reentrant route search is not entered twice. Nothing is calculated from
display(), draw() or sync_step. It also works while the game is paused, which a step() hook
would not have.

Route and stops stay on separate channels: obj_t::highlight is still only the stops, and the
route is an ordered koord3d list that main_view_t::display draws as a polyline in the player
colour. A leg with no route appends an explicit break that the renderer never joins across, so
disconnected stops never get a false straight line between them.

For the vehicle, one path covers all three cases: a throwaway vehicle_desc_t handed to
vehicle_builder_t::build with the owner of the convoy or line, flagged not_on_map and deleted
when the legs are done - the same recipe simworld.cc already uses for its map generation way
search. A real convoy is never used as the test driver and never modified. A line with convoys
takes the speed of its first one; a line without convoys gets a generic 500 km/h vehicle, which
is what you meant by driving a generic vehicle for the route.

Two limits I would rather state than hide.

Electrification is not enforced. Both rail_vehicle_t::check_next_tile and
road_vehicle_t::check_next_tile take it from cnv->needs_electrification(), and a temporary
vehicle has no convoy, so the drawn route can use unelectrified way that an electric convoy
could not. This is inherent to the temporary-vehicle approach and the map generation driver has
it too. Enforcing it means giving the temporary vehicle a convoy, which felt well outside what
you asked for - tell me if you want that instead.

Air is not drawn at all. air_vehicle_t finds its own route through the runway search rather than
through calc_route, so a fresh air vehicle would only follow taxiways and would search the whole
map before failing. Skipping it seemed better than a slow wrong answer.

The owner is honoured: private ways and access restrictions are checked through get_owner_nr(),
so two players get different routes for the same schedule, and there is a test for that.

Numbers: 4 files, +216 lines, no deletions - down from 10 files and +372. The product patch
applies to a clean r12110 tree and builds there on its own with no warnings in the four changed
files. The standard suite is 213/213 with 12 new tests, deterministic across runs.

I also ran the new tests against eight deliberate breakages of the code they cover. Six were
caught. One was not, and it was the useful one: the stale-result guard in the step function
could never fire, because the pending request and the overlay ownership are always set and
cleared together - so that guard and one field went away and the code got smaller. The other one
that was not caught is the in-place refresh while the window stays open; the headless backend
always reports an empty clip rect, so the GUI component never draws and the test cannot reach
it. That path is verified in the graphical demo instead, and I have said so rather than counting
it as covered.

Screenshots of the cycle are attached: road route with two corners, the route updating when a
stop is added, a second schedule replacing the first, a line with no convoys, and both overlays
gone after closing.

The zip holds two patches, deliberately separate. schedule-route-overlay-r12110.patch is the
feature and the only one meant for trunk. schedule-route-overlay-tests-r12110.patch is the test
support: the 12 scenario tests plus the small C++ file that lets them drive the real
gui_schedule_t component and read the real world state instead of a copy of the logic. Apply the
first alone if you only want the feature, both if you want to run the tests. demo-scenario.nut
is what produced the screenshots and is not meant for trunk either.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

prissi

Thank you, that sounds very good. I wonder, if the electric flag cannot be set from the convoi to the front vehicle after a schedule change. Then it could be used by the testdriver, too. I guess the change would be just a few more lines in simvehikel.

victor_18993

You were right about putting the electrification flag on the vehicle, and your size estimate was right too. The flag itself is +31/-7 across six files.

convoi_t::needs_electric remains the source of truth. A new set_needs_electrification() setter updates it and propagates the value to the vehicles. It replaces the three direct assignments in add_vehicle(), remove_vehicle_at(), and the load branch of rdwr().

rail_vehicle_t::check_next_tile() and road_vehicle_t::check_next_tile() now read the flag from the vehicle. The temporary driver used by the schedule overlay receives the flag from the convoi whose schedule is being edited, or from the first convoi of the line.

A line without convois still uses the generic vehicle, so it is not assumed to require catenary.

I made one deliberate adjustment to your suggestion: the flag is copied to every vehicle in the convoi, not only the first one. check_next_tile() is also called while the convoi is moving, from vehicle_t::hop_check(), and each vehicle calls it on itself. Setting it only on the first vehicle would remove the existing catenary check from the others.

set_convoi(NULL) clears the flag, so a vehicle left in a depot cannot keep a stale electrification restriction. The flag is derived state and is not saved. rdwr() recalculates it when loading, so the savegame format is unchanged.

I left three related things outside this patch:

- rail_vehicle_t::is_target() still reads through cnv, but it dereferences cnv unconditionally shortly afterwards, so changing it here would not add anything.
- Rail minimum-speed signs also read through the convoi. A temporary driver therefore ignores them, unlike the equivalent road check. That looks like a separate issue.
- Air routes are still not shown because air_vehicle_t uses runway search rather than calc_route().

I added four electrification tests using the same map with two possible corridors: a short unelectrified route and a longer fully electrified one.

They verify that:

- an electric convoi uses the longer electrified route;
- a diesel convoi uses the shorter route;
- removing the catenary leaves the electric convoi with no route while the editor still owns the overlay;
- the restriction remains correct after changing the schedule.

The updated product patch is 10 files, +258/-7, against clean r12117.

Results:

- full suite: 222/222;
- overlay tests alone: 16/16;
- two consecutive runs produced the same test sequence;
- no new warnings in the files touched by the patch;
- savegame format unchanged.

The product and test patches were also applied and compiled independently on a fresh r12117 worktree.

One requirement for running the tests is:

-theme classic.tab

These tests instantiate a real gui_schedule_t. Headless builds intentionally load no GUI theme, so they need one explicitly. The requirement is documented in the test file.

The separate question about gui_schedule_t accessing the theme without a guard is kept out of this patch and discussed in its own bug thread.
En la vida todo son vivencias y cada una de ellas nos hace mas grandes,¿Como de grande eres tu? :)

prissi

A few corrections. I think one should either use the internal flag or the getter functions. C-wise, it would be the flag, but maybe I am too oldschool.

For the same reason, I removed the convoi electrification check from target. I think, the same should be true for the minimum rail sign which I did not touch.

I would strongly suggest that check_next_tile is restricted to the leading car. I have set this for your patch and did not find any issues even when adding
assert(leading);
to check_new_tile() for vehikel_t

Also, I found the route to have many zigzags on my testgames when there is actually diagonals. Below is a test, which makes diagonals, but does not get the corners right. Maybe you have an idea of how to fix it.

Finally, when zooming in, maybe the line should get thicker as well.

If you can solve the diagonals, then sumbit. I sent you an email with the svn password.

// route of the schedule shown by a schedule editor (display only, see karte_t)
const vector_tpl<koord3d> &schedule_route = welt->get_schedule_route();
if(  !schedule_route.empty()  ) {
DBG_DEBUG4("main_view_t::display", "display schedule route");
const player_t *pl = welt->get_player( welt->get_schedule_route_player_nr() );
const PIXVAL col  = gfx->palette_lookup( pl ? pl->get_player_color1()+3 : COL_WHITE );
const PIXVAL dark = gfx->palette_lookup( COL_BLACK );
// centre of the top surface of a tile, in the internal 64 per tile units
const koord centre( OBJECT_OFFSET_STEPS*2, OBJECT_OFFSET_STEPS*3 );

bool has_prev = false;
bool has_prev_prev = false;
scr_coord previous, here;
ribi_t::ribi last_r = 0;
koord3d last_pos = koord3d::invalid;
koord3d last_last_pos = koord3d::invalid;
for ( const koord3d &pos : schedule_route) {
if(  pos == koord3d::invalid  ) {
if (last_r) {
// still segment missing
gfx->draw_line(last_pos.x, previous.y + 1, here.x, here.y + 1, dark);
gfx->draw_line(previous.x, previous.y, here.x, here.y, col);
}
// no route for this leg: never draw a line across the gap
has_prev = false;
has_prev_prev = false;
last_r = 0;
last_pos = koord3d::invalid;
last_last_pos = koord3d::invalid;
continue;
}
here = viewport->get_screen_coord( pos, centre );
if(  has_prev  &&  !has_prev_prev  ) {
// first segment is always straight
// a dark line below the coloured one, to stay visible on any ground
gfx->draw_line( last_pos.x, previous.y+1, here.x, here.y+1, dark );
gfx->draw_line( previous.x, previous.y,   here.x, here.y,   col );
previous = here;
last_r = ribi_type(pos, last_pos);
}
else if(has_prev_prev) {
ribi_t::ribi new_r = ribi_type(pos, last_last_pos);
if (new_r != last_r) {
// changed direction, drawn full segment in one run (and make diagonals)
// a dark line below the coloured one, to stay visible on any ground
gfx->draw_line(last_pos.x, previous.y + 1, here.x, here.y + 1, dark);
gfx->draw_line(previous.x, previous.y, here.x, here.y, col);
previous = here;
last_r = new_r;
}
}
last_last_pos = last_pos;
last_pos = pos;
if (has_prev) {
has_prev_prev = true;
}
has_prev = true;
}
}