Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions MODS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ what it says and invents nothing:
depth_map: Some("example/assets/track/ballast_disp.jpg"), // height map -> parallax relief
occlusion_map: Some("example/assets/track/ballast_ao.jpg"),// ambient occlusion between the stones
texture_scale: 2.2, // metres one repeat of those four covers, along and across
color: (0.32, 0.30, 0.28), // untextured fallback; the route editor tints sections its own way
color: (0.32, 0.30, 0.28), // colour of a type that names no texture; the editor tints sections its own way
roughness: 1.0, // scales the rolling noise; jointed track > 1, slab track < 1
reverb: 0.0, // how much the surroundings ring: 0 = open line, 1 = tunnel
max_speed: 250.0, // superstructure limit [km/h], caps the line's speed profile
Expand Down Expand Up @@ -1366,13 +1366,20 @@ linear; the loader is told so and does not gamma-decode them. A type that names
textures is skinned in its `color`.

A line assigns types per edge as steps over the arc length, so one edge changes its
superstructure section by section; the reserved name `"default"` returns to the built-in
default type:
superstructure section by section:

```ron
track_type: [(0.0, "example:hauptbahn"), (3000.0, "example:altbau")],
```

**Every track has to name one.** There is no built-in type and no reserved
`"default"` name: what a track is built of is part of the module, not of the
engine, and a module that leaves it out does not compile — the compiler reports
`MissingTrackType(<edge>)` and the route editor's rule check says the same thing
before the run. A type from another mod is a dependency like any other; name that
mod in `depends:`. This is deliberate: a silent fallback is how a line ends up on
untextured grey ballast without anybody being told.

`max_speed` merges into the speed profile every consumer already reads (AI, LZB, HUD,
scoring); `roughness` reaches the sound table as the `Roughness` quantity (the default
rolling entries carry a volume factor on it, see Sounds); `reverb` drives the reverb the
Expand All @@ -1381,8 +1388,9 @@ or a deep cutting sits around 0.3 … 0.6. Modelling the room on the track type
on the terrain is the same trade `roughness` makes: a line says where its tunnels are by
assigning the type, and nothing has to trace geometry at run time. The route editor edits the
sections in the selection panel (a color chip per section, the map tints the track ribbon
to match) and its rule check flags names no installed mod has, and LZB types on a line
that places no line conductor.
to match) and its rule check flags tracks that name no type at all, names no installed
mod has, and LZB types on a line that places no line conductor. The lay tool arms itself
with the first installed type, so track drawn in the editor carries one from the start.

### Electrification

Expand Down
6 changes: 4 additions & 2 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ As of 2026-08-31 · `cargo test --workspace`: **1136 tests green** · clippy and
**Track types** (superstructure classes, `track_types/*.ron` in a mod): texture, color,
roughness, how much its surroundings ring (`reverb`, 0 = open line, 1 = tunnel),
superstructure speed limit and an LZB flag — assigned per edge as a step
profile over `s`, so one edge changes its type section by section, with the reserved
name `"default"` returning to the built-in type. The mod runtime resolves the names
profile over `s`, so one edge changes its type section by section. **Every track has
to name one**: there is no built-in type and no reserved `"default"` name, and a
module that leaves it out does not compile (`CompileError::MissingTrackType`) —
a silent fallback is how a line ends up on untextured grey ballast unnoticed. The mod runtime resolves the names
after compile (like signal types) and merges `max_speed` into the one speed profile AI,
LZB, HUD and scoring already read; the app **builds the track the type describes**
(`world_render::track`, rebuilt 2026-08-31 against the DB InfraGO dimensions and split
Expand Down
9 changes: 6 additions & 3 deletions crates/app/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,10 +476,13 @@ pub fn update_audio(
SoundState::sample(vehicle, &cab, protection, audio.previous.get(&(t, v)), dt);
// The sampler deliberately sees no track and no weather — both are filled in
// here, where net and world state live.
// Track without a type is track nobody has laid a superstructure
// on — it cannot happen on a compiled line, and what rolls over it
// rolls as smoothly as welded main line.
state.roughness = sim
.net
.track_type_at(vehicle.pos.edge, vehicle.pos.s)
.roughness;
.map_or(1.0, |ty| ty.roughness);
// The rain quantity is how hard it falls, not whether it does: a
// drizzle is not a downpour with the volume turned down.
let weather = sim.weather.now;
Expand Down Expand Up @@ -530,10 +533,10 @@ pub fn update_audio(
.trains
.get(player.0)
.and_then(|train| train.vehicles.first())
.map(|vehicle| {
.and_then(|vehicle| {
sim.net
.track_type_at(vehicle.pos.edge, vehicle.pos.s)
.reverb
.map(|ty| ty.reverb)
})
.unwrap_or(0.0);
audio
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2049,7 +2049,7 @@ fn update_precipitation(
.0
.net
.track_type_at(vehicle.pos.edge, vehicle.pos.s)
.reverb
.map_or(0.0, |ty| ty.reverb)
.clamp(0.0, 1.0) as f32;
// The wind the drops actually meet: the weather's own — swaying in strength
// and direction with the gusts, because a curtain of rain never stands at one
Expand Down
13 changes: 9 additions & 4 deletions crates/content/src/area_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn line() -> LineSource {
grade: vec![],
cant: vec![],
speed: vec![],
track_type: vec![],
track_type: vec![(0.0, STARTER_TRACK_TYPE.into())],
electrification: vec![],
formation: true,
}],
Expand Down Expand Up @@ -139,7 +139,7 @@ fn an_area_can_switch_the_wire_and_the_superstructure() {
let inside = net.edges()[0].track_type.at(1500.0);
let outside = net.edges()[0].track_type.at(500.0);
assert_ne!(inside, outside);
assert_eq!(outside, 0, "outside the area, the default type");
assert_eq!(outside, 0, "outside the area, the track's own type");
assert_eq!(net.types()[inside as usize].name, "example:nebenbahn");
}

Expand Down Expand Up @@ -200,7 +200,7 @@ fn removing_a_track_takes_its_spans_with_it() {
grade: vec![],
cant: vec![],
speed: vec![],
track_type: vec![],
track_type: vec![(0.0, STARTER_TRACK_TYPE.into())],
electrification: vec![],
formation: true,
});
Expand Down Expand Up @@ -234,7 +234,12 @@ fn a_span_is_stored_the_way_round_it_was_marked() {
#[test]
fn the_rule_check_finds_a_marking_that_does_not_reach_the_line() {
use std::collections::BTreeMap;
let types = BTreeMap::new();
// The line is laid with the starter type; the registry answers it, so the
// check reports the markings and not a missing mod.
let types = BTreeMap::from([(
STARTER_TRACK_TYPE.to_string(),
track_model::TrackType::placeholder(STARTER_TRACK_TYPE),
)]);
let objects = BTreeMap::new();

// A marking with no properties yet: useful while working, worth saying out loud.
Expand Down
11 changes: 9 additions & 2 deletions crates/content/src/bin/import-line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//!
//! ```text
//! import-line line.json [--dgm dgm.xyz --epsg 25832] [--name "Musterbahn"]
//! [--sample 20] [--smoothing 3] [--out line.ron]
//! [--sample 20] [--smoothing 3] [--track-type example:hauptbahn]
//! [--out line.ron]
//! ```

use content::import::dgm::TerrainSource;
Expand All @@ -14,7 +15,8 @@ fn main() -> ExitCode {
if args.is_empty() || args[0] == "--help" {
eprintln!(
"Usage: import-line <overpass.json> [--dgm <file.xyz> --epsg <25832>] \
[--name <name>] [--sample <m>] [--smoothing <n>] [--no-snap] [--max-cant <mm>] [--out <file.ron>]"
[--name <name>] [--sample <m>] [--smoothing <n>] [--no-snap] \
[--max-cant <mm>] [--track-type <mod:name>] [--out <file.ron>]"
);
return ExitCode::from(2);
}
Expand Down Expand Up @@ -86,6 +88,11 @@ fn main() -> ExitCode {
if let Some(v) = flag("--start-way").and_then(|v| v.parse().ok()) {
options.start_way = Some(v);
}
// What the imported track is built of. Every edge is written out with it —
// a line that names no type does not compile.
if let Some(v) = flag("--track-type") {
options.track_type = v;
}

let (line, report) = match import_line(&osm_json, grid.as_ref(), &options) {
Ok(v) => v,
Expand Down
5 changes: 3 additions & 2 deletions crates/content/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ fn shift_payload(kind: &DeviceKind, payload: &str, off: ModuleOffsets) -> String
mod tests {
use super::*;
use crate::route::{
BoundarySource, DeviceSource, EdgeSource, GeoPoint, SectionSource, SignalSource,
BoundarySource, DeviceSource, EdgeSource, GeoPoint, STARTER_TRACK_TYPE, SectionSource,
SignalSource,
};
use sim_core::interlock::{SignalKind, SignalSystem};
use track_model::{Segment, TrackPosition};
Expand All @@ -493,7 +494,7 @@ mod tests {
grade: vec![],
cant: vec![],
speed: vec![(0.0, 120.0)],
track_type: vec![],
track_type: vec![(0.0, STARTER_TRACK_TYPE.into())],
electrification: Vec::new(),
formation: true,
}],
Expand Down
10 changes: 7 additions & 3 deletions crates/content/src/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ fn demo_trees() -> Vec<TreeSource> {
///
/// Signalling: distant signal at km 1.0 and main signal at km 2.0 (end of block),
/// plus the three PZB magnets. From the third section on there is a line cable (LZB).
///
/// It names its superstructure like any other line does
/// ([`crate::route::STARTER_TRACK_TYPE`]) — there is no built-in type, so a
/// demo line that said nothing would stand on no ballast at all.
pub fn musterbahn() -> LineSource {
let magnet = |p: &MagnetPayload| ron::to_string(p).unwrap();

Expand All @@ -189,7 +193,7 @@ pub fn musterbahn() -> LineSource {
grade: vec![],
cant: vec![],
speed: vec![(0.0, 160.0)],
track_type: vec![],
track_type: vec![(0.0, crate::route::STARTER_TRACK_TYPE.to_string())],
electrification: Vec::new(),
formation: true,
},
Expand All @@ -205,7 +209,7 @@ pub fn musterbahn() -> LineSource {
grade: vec![],
cant: vec![(0.0, 0.0), (200.0, 80.0), (800.0, 0.0)],
speed: vec![(0.0, 130.0)],
track_type: vec![],
track_type: vec![(0.0, crate::route::STARTER_TRACK_TYPE.to_string())],
electrification: Vec::new(),
formation: true,
},
Expand All @@ -217,7 +221,7 @@ pub fn musterbahn() -> LineSource {
grade: vec![(0.0, 0.0), (500.0, 8.0), (2500.0, 0.0)],
cant: vec![],
speed: vec![(0.0, 160.0)],
track_type: vec![],
track_type: vec![(0.0, crate::route::STARTER_TRACK_TYPE.to_string())],
electrification: Vec::new(),
formation: true,
},
Expand Down
11 changes: 10 additions & 1 deletion crates/content/src/import/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ pub struct ImportOptions {
/// What the imported line is electrified with (an id of
/// [`track_model::PowerSystem`], or `"none"`).
pub electrification: String,
/// What the imported track is built of (`"<mod>:<name>"` of a
/// `track_types/*.ron`). Every edge is written out with it: there is no
/// built-in type, so an import that named none would produce a module
/// that does not compile.
pub track_type: String,
/// Permitted speed when OSM does not give one [km/h].
pub default_speed: f64,
/// Height used when no DGM is available [m].
Expand All @@ -52,6 +57,7 @@ impl Default for ImportOptions {
max_edge_length: 2_000.0,
geoid_offset: 46.0,
electrification: track_model::PowerSystem::Ac15kv.id().to_string(),
track_type: crate::route::STARTER_TRACK_TYPE.to_string(),
default_speed: 100.0,
default_height: 100.0,
start_way: None,
Expand Down Expand Up @@ -183,7 +189,10 @@ pub fn import_line(
grade: shift_profile(&fitted.grade, offset, len),
cant: shift_profile(&fitted.cant, offset, len),
speed: shift_profile(&fitted.speed, offset, len),
track_type: vec![],
// The superstructure belongs to the track the same way the wire
// does, and it is not optional: every imported edge is written out
// with the type the import was given.
track_type: vec![(0.0, options.track_type.clone())],
// The wire belongs to the track: every imported edge starts out
// saying so itself, so it can be changed one edge at a time.
electrification: vec![(0.0, options.electrification.clone())],
Expand Down
Loading
Loading