veloren_rtsim/data/
airship.rs

1use crate::data::Npcs;
2use common::{rtsim::NpcId, terrain::MapSizeLg};
3use std::cmp::Ordering;
4use vek::*;
5use world::{
6    World,
7    civ::airship_travel::{
8        AirshipDockingApproach, AirshipFlightPhase, AirshipSpawningLocation, Airships,
9    },
10    util::DHashMap,
11};
12
13/// Data for airship operations. This is part of RTSimData and is NOT persisted.
14#[derive(Clone, Default, Debug)]
15pub struct AirshipSim {
16    /// The pilot route assignments. The key is the pilot NpcId, the value is
17    /// a tuple. The first element is the index for the outer Airships::routes
18    /// Vec (the route loop index), and the second element is the index for the
19    /// pilot's initial route leg in the inner Airships::routes Vec.
20    pub assigned_routes: DHashMap<NpcId, (usize, usize)>,
21
22    /// The pilots assigned to a route in the order they fly the route.
23    pub route_pilots: DHashMap<usize, Vec<NpcId>>,
24}
25
26#[cfg(debug_assertions)]
27macro_rules! debug_airships {
28    ($level:expr, $($arg:tt)*) => {
29        match $level {
30            0 => tracing::info!($($arg)*),
31            1 => tracing::warn!($($arg)*),
32            2 => tracing::error!($($arg)*),
33            3 => tracing::debug!($($arg)*),
34            4 => tracing::trace!($($arg)*),
35            _ => tracing::trace!($($arg)*),
36        }
37    }
38}
39
40#[cfg(not(debug_assertions))]
41macro_rules! debug_airships {
42    ($($arg:tt)*) => {};
43}
44
45impl AirshipSim {
46    /// Called from world generation code to set the route and initial leg
47    /// indexes for an airship captain NPC. World generation is dynamic and
48    /// can change across runs, and existing captain (and ship) NPCs may
49    /// change the assigned route and leg, and new NPCs may be added to the
50    /// world. This is the function that connects the saved RTSim data to
51    /// the world generation data.
52    pub fn register_airship_captain(
53        &mut self,
54        location: &AirshipSpawningLocation,
55        captain_id: NpcId,
56        airship_id: NpcId,
57        world: &World,
58        npcs: &mut Npcs,
59    ) {
60        self.assigned_routes
61            .insert(captain_id, (location.route_index, location.leg_index));
62
63        assert!(
64            location.dir.is_normalized(),
65            "Airship direction {:?} is not normalized",
66            location.dir
67        );
68        let airship_wpos3d = match location.flight_phase {
69            AirshipFlightPhase::DepartureCruise
70            | AirshipFlightPhase::ApproachCruise
71            | AirshipFlightPhase::Transition => location.pos.with_z(
72                world
73                    .sim()
74                    .get_alt_approx(location.pos.map(|e| e as i32))
75                    .unwrap_or(0.0)
76                    + location.height,
77            ),
78            _ => location.pos.with_z(location.height),
79        };
80        let airship_mount_offset = if let Some(airship) = npcs.get_mut(airship_id) {
81            airship.wpos = airship_wpos3d;
82            airship.dir = location.dir;
83            airship.body.mount_offset()
84        } else {
85            tracing::warn!(
86                "Failed to find airship {:?} for captain {:?}",
87                airship_id,
88                captain_id,
89            );
90            Vec3::new(0.0, 0.0, 0.0)
91        };
92        if let Some(captain) = npcs.get_mut(captain_id) {
93            let captain_pos = airship_wpos3d
94                + Vec3::new(
95                    location.dir.x * airship_mount_offset.x,
96                    location.dir.y * airship_mount_offset.y,
97                    airship_mount_offset.z,
98                );
99            captain.wpos = captain_pos;
100            captain.dir = location.dir;
101        }
102
103        debug_airships!(
104            4,
105            "Registering airship {:?}/{:?} for spawning location {:?}",
106            airship_id,
107            captain_id,
108            location,
109        );
110    }
111
112    /// Called from world generation code after all airship captains have been
113    /// registered. This function generates the route_pilots hash map which
114    /// provides a list of pilots assigned to each route index, in the order
115    /// they will fly the route. This provides for determining the "next
116    /// pilot" on a route, which is used for deconfliction and
117    /// "traffic control" of airships.
118    pub fn configure_route_pilots(&mut self, airships: &Airships, npcs: &Npcs) {
119        debug_airships!(4, "Airship Assigned Routes: {:?}", self.assigned_routes);
120        // for each route index and leg index, find all pilots that are assigned to
121        // the route index and leg index. Sort them by their distance to the starting
122        // position of the leg. Repeat for all legs of the route, then add the resulting
123        // list to the route_pilots hash map.
124
125        for route_index in 0..airships.route_count() {
126            let mut pilots_on_route = Vec::new();
127            for leg_index in 0..airships.docking_site_count_for_route(route_index) {
128                // Find all pilots that are spawned on the same route_index and leg_index.
129                let mut pilots_on_leg: Vec<_> = self
130                    .assigned_routes
131                    .iter()
132                    .filter(|(_, (rti, li))| *rti == route_index && *li == leg_index)
133                    .map(|(pilot_id, _)| *pilot_id)
134                    .collect();
135
136                if !pilots_on_leg.is_empty() {
137                    // Sort pilots by their distance to the starting position of the leg.
138                    let start_pos = airships.route_leg_departure_location(route_index, leg_index);
139                    pilots_on_leg.sort_by(|&pilot1, &pilot2| {
140                        let pilot1_pos = npcs.get(pilot1).map_or(start_pos, |npc| npc.wpos.xy());
141                        let pilot2_pos = npcs.get(pilot2).map_or(start_pos, |npc| npc.wpos.xy());
142                        start_pos
143                            .distance_squared(pilot1_pos)
144                            .partial_cmp(&start_pos.distance_squared(pilot2_pos))
145                            .unwrap_or(Ordering::Equal)
146                    });
147                    pilots_on_route.extend(pilots_on_leg);
148                }
149            }
150            if !pilots_on_route.is_empty() {
151                debug_airships!(4, "Route {} pilots: {:?}", route_index, pilots_on_route);
152                self.route_pilots.insert(route_index, pilots_on_route);
153            }
154        }
155    }
156
157    /// Retrieves two airship destinations - the current and the next.
158    pub fn next_destinations(
159        &self,
160        airships: &Airships,
161        map: &MapSizeLg,
162        route_index: usize,
163        current_leg: Option<(usize, AirshipFlightPhase)>,
164    ) -> Option<(AirshipDockingApproach, AirshipDockingApproach)> {
165        if let Some(current_leg) = current_leg {
166            match current_leg.1 {
167                // If docked, retrieve the next two.
168                AirshipFlightPhase::Docked | AirshipFlightPhase::Ascent => {
169                    let next_route_leg = airships.increment_route_leg(route_index, current_leg.0);
170                    Some((
171                        airships.approach_for_route_and_leg(route_index, next_route_leg, map),
172                        airships.approach_for_route_and_leg(
173                            route_index,
174                            airships.increment_route_leg(route_index, next_route_leg),
175                            map,
176                        ),
177                    ))
178                },
179                // If not docked, retrieve the current and next.
180                AirshipFlightPhase::ApproachCruise
181                | AirshipFlightPhase::DepartureCruise
182                | AirshipFlightPhase::Descent
183                | AirshipFlightPhase::Transition => Some((
184                    airships.approach_for_route_and_leg(route_index, current_leg.0, map),
185                    airships.approach_for_route_and_leg(
186                        route_index,
187                        airships.increment_route_leg(route_index, current_leg.0),
188                        map,
189                    ),
190                )),
191            }
192        } else {
193            None
194        }
195    }
196
197    /// Given a route index and pilot id, find the next pilot on the route (the
198    /// one that is ahead of the given pilot).
199    pub fn next_pilot(&self, route_index: usize, pilot_id: NpcId) -> Option<NpcId> {
200        if let Some(pilots) = self.route_pilots.get(&route_index) {
201            if pilots.len() < 2 {
202                // If there is only one pilot on the route, return the pilot itself.
203                tracing::warn!(
204                    "Route {} has only one pilot, 'next_pilot' doesn't make sense.",
205                    route_index,
206                );
207                return None;
208            }
209            if let Some(pilot_index) = pilots.iter().position(|&p_id| p_id == pilot_id) {
210                if pilot_index == pilots.len() - 1 {
211                    // If the pilot is the last one in the list, return the first one.
212                    return Some(pilots[0]);
213                } else {
214                    // Otherwise, return the next pilot in the list.
215                    return Some(pilots[pilot_index + 1]);
216                }
217            }
218        }
219        tracing::warn!(
220            "Failed to find next pilot for route index {} and pilot id {:?}",
221            route_index,
222            pilot_id
223        );
224        None
225    }
226}