paiagram/rw_data/
jgrpp.rs

1use bevy::{platform::collections::HashMap, prelude::*};
2use moonshine_core::kind::{InsertInstance, Instance};
3use serde::Deserialize;
4
5use crate::{
6    graph::{Graph, Station},
7    rw_data::ModifyData,
8    units::time::{Duration, TimetableTime},
9    vehicles::{
10        Vehicle,
11        entries::{TimetableEntry, TravelMode, VehicleSchedule},
12        vehicle_set::VehicleSet,
13    },
14};
15
16#[derive(Deserialize)]
17#[serde(rename_all = "kebab-case")]
18struct Root {
19    #[serde(rename = "version")]
20    schema_version: u32,
21    #[serde(rename = "vehicle-group-name")]
22    name: String,
23    game_properties: GameProperties,
24    #[serde(default)]
25    schedules: Vec<ExportSchedule>,
26    orders: Vec<ExportOrder>,
27}
28
29#[derive(Deserialize)]
30#[serde(rename_all = "kebab-case")]
31struct GameProperties {
32    ticks_per_minute: u32,
33}
34
35#[derive(Deserialize)]
36#[serde(rename_all = "kebab-case")]
37struct ExportSchedule {
38    slots: Vec<i32>,
39    duration: i32,
40}
41
42#[derive(Deserialize)]
43#[serde(rename_all = "kebab-case")]
44struct ExportOrder {
45    // #[serde(rename = "type")]
46    // order_type: OrderType,
47    // stopping_pattern: Option<StoppingPattern>,
48    destination_id: u32,
49    destination_name: String,
50    destination_location: DestinationPosition,
51    travel_time: i32,
52    wait_time: Option<i32>,
53    // wait_fixed: Option<bool>,
54    // stop_location: u32,
55    schedule_index: Option<usize>,
56}
57
58// #[derive(Deserialize)]
59// #[serde(rename_all = "kebab-case")]
60// enum OrderType {
61//     GoToStation,
62//     GoToWaypoint,
63// }
64//
65// #[derive(Deserialize)]
66// #[serde(rename_all = "kebab-case")]
67// enum StoppingPattern {
68//     GoNonstopVia,
69// }
70
71#[derive(Deserialize, Clone, Copy)]
72struct DestinationPosition {
73    #[serde(rename = "X")]
74    x: f32,
75    #[serde(rename = "Y")]
76    y: f32,
77}
78
79impl Into<egui::Pos2> for DestinationPosition {
80    fn into(self) -> egui::Pos2 {
81        egui::Pos2 {
82            x: self.x,
83            y: self.y,
84        }
85    }
86}
87
88fn make_destination(
89    destination_map: &mut HashMap<u32, Instance<Station>>,
90    commands: &mut Commands,
91    graph: &mut Graph,
92    order: &ExportOrder,
93) -> Instance<Station> {
94    if let Some(s) = destination_map.get(&order.destination_id) {
95        return *s;
96    };
97    // not found in list, insert instead
98    let station = commands
99        .spawn((Name::new(order.destination_name.clone()),))
100        .insert_instance(Station(order.destination_location.into()))
101        .into();
102    destination_map.insert(order.destination_id, station);
103    graph.add_node(station);
104    station
105}
106
107pub fn load_jgrpp_timetable_export(
108    mut commands: Commands,
109    mut reader: MessageReader<ModifyData>,
110    mut graph: ResMut<Graph>,
111) {
112    let mut str: Option<&[String]> = None;
113    for modification in reader.read() {
114        match modification {
115            ModifyData::LoadJGRPP(s) => str = Some(s.as_slice()),
116            _ => {}
117        }
118    }
119    let Some(str) = str else {
120        return;
121    };
122    let mut destination_map: HashMap<u32, Instance<Station>> = HashMap::new();
123    let vehicle_set_entity = commands
124        .spawn((Name::new("JGRPP import set"), VehicleSet))
125        .id();
126    for maybe_root in str {
127        let root: Root = match serde_json::from_str(maybe_root) {
128            Ok(root) => root,
129            Err(e) => {
130                error!("Error while deserializing JGRPP timetable export: {:?}", e);
131                continue;
132            }
133        };
134        let tick_to_duration = |tick: i32| {
135            let mins = tick as f32 / root.game_properties.ticks_per_minute as f32;
136            Duration((mins * 60.0) as i32)
137        };
138        assert_eq!(root.schema_version, 1, "Version number must be 1!");
139        // dyn, but this is trivial enough and it only runs once
140        let schedules_iter: Box<dyn Iterator<Item = Vec<&ExportOrder>>> =
141            if root.schedules.is_empty() {
142                // simply chain from the start to the end
143                Box::new(std::iter::once(
144                    root.orders.iter().chain(root.orders.first()).collect(),
145                ))
146            } else {
147                // wrap around the schedule to make sure those orders with scheduled dispatch settings are
148                // always moved to the front.
149                let first_trigger = root
150                    .orders
151                    .iter()
152                    .position(|e| e.schedule_index.is_some())
153                    .unwrap_or(0);
154                let mut rotated = root.orders[first_trigger..]
155                    .iter()
156                    .chain(root.orders[..=first_trigger].iter())
157                    .peekable();
158                Box::new(std::iter::from_fn(move || {
159                    let mut group = Vec::new();
160                    if let Some(first) = rotated.next() {
161                        group.push(first);
162                    } else {
163                        return None;
164                    }
165                    while let Some(item) = rotated.peek() {
166                        if item.schedule_index.is_some() {
167                            group.push(*item);
168                            break;
169                        } else {
170                            group.push(rotated.next().unwrap());
171                        }
172                    }
173                    if group.len() > 1 { Some(group) } else { None }
174                }))
175            };
176        for raw_schedule in schedules_iter {
177            let vehicle_entity = commands.spawn((Name::new(root.name.clone()), Vehicle)).id();
178            commands
179                .entity(vehicle_set_entity)
180                .add_child(vehicle_entity);
181            let mut vehicle_schedule = VehicleSchedule {
182                start: TimetableTime(0),
183                repeat: None,
184                departures: Vec::new(),
185                entities: Vec::new(),
186            };
187            let mut time_counter = TimetableTime(0);
188            for order in raw_schedule {
189                let station_instance =
190                    make_destination(&mut destination_map, &mut commands, &mut graph, order);
191                let entry = TimetableEntry {
192                    station: station_instance.entity(),
193                    arrival: {
194                        let a = TravelMode::At(time_counter);
195                        time_counter += tick_to_duration(order.travel_time);
196                        a
197                    },
198                    departure: order.wait_time.map(|t| {
199                        let a = TravelMode::At(time_counter);
200                        time_counter += tick_to_duration(t);
201                        a
202                    }),
203                    service: None,
204                    track: None,
205                };
206                let entry_entity = commands.spawn(entry).id();
207                commands.entity(vehicle_entity).add_child(entry_entity);
208                if let Some(schedule_index) = order.schedule_index
209                    && vehicle_schedule.repeat.is_none()
210                {
211                    let schedule = &root.schedules[schedule_index];
212                    vehicle_schedule.repeat = Some(tick_to_duration(schedule.duration));
213                    vehicle_schedule
214                        .departures
215                        .extend(schedule.slots.iter().cloned().map(tick_to_duration));
216                }
217                vehicle_schedule.entities.push(entry_entity);
218            }
219            if vehicle_schedule.repeat.is_none() {
220                vehicle_schedule.repeat = Some(time_counter.as_duration());
221                vehicle_schedule.departures = vec![Duration(0)]
222            }
223            commands.entity(vehicle_entity).insert(vehicle_schedule);
224        }
225    }
226}