veloren_world/civ/
airship_travel.rs

1use crate::{
2    Index,
3    sim::WorldSim,
4    site::{self, Site, plot::PlotKindMeta},
5    util::{DHashMap, DHashSet, seed_expan},
6};
7use common::{
8    store::{Id, Store},
9    terrain::{MapSizeLg, TERRAIN_CHUNK_BLOCKS_LG},
10    util::Dir,
11};
12use delaunator::{Point, Triangulation, triangulate};
13use itertools::Itertools;
14use rand::{SeedableRng, prelude::*};
15use rand_chacha::ChaChaRng;
16use tracing::error;
17use vek::*;
18
19#[cfg(feature = "airship_maps")]
20use crate::civ::airship_route_map::*;
21
22#[cfg(debug_assertions)]
23macro_rules! debug_airships {
24    ($level:expr, $($arg:tt)*) => {
25        match $level {
26            0 => tracing::info!($($arg)*),
27            1 => tracing::warn!($($arg)*),
28            2 => tracing::error!($($arg)*),
29            3 => tracing::debug!($($arg)*),
30            4 => tracing::trace!($($arg)*),
31            _ => tracing::trace!($($arg)*),
32        }
33    }
34}
35
36#[cfg(not(debug_assertions))]
37macro_rules! debug_airships {
38    ($($arg:tt)*) => {};
39}
40
41/// A docking position (id, position). The docking position id is
42/// an index of all docking positions in the world.
43#[derive(Clone, Copy, Debug, Default, PartialEq)]
44pub struct AirshipDockingPosition(pub u32, pub Vec3<f32>);
45
46/// The AirshipDock Sites are always oriented along a cardinal direction.
47/// The docking platforms are likewise on the sides of the dock perpendicular
48/// to a cardinal axis.
49#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
50pub enum AirshipDockPlatform {
51    #[default]
52    NorthPlatform,
53    EastPlatform,
54    SouthPlatform,
55    WestPlatform,
56}
57
58/// An airship can dock with its port or starboard side facing the dock.
59#[derive(Debug, Copy, Clone, PartialEq, Default)]
60pub enum AirshipDockingSide {
61    #[default]
62    Port,
63    Starboard,
64}
65
66impl AirshipDockingSide {
67    const EAST_REF_VEC: Vec2<f32> = Vec2 { x: 1.0, y: 0.0 };
68    const NORTH_REF_VEC: Vec2<f32> = Vec2 { x: 0.0, y: 1.0 };
69    const SOUTH_REF_VEC: Vec2<f32> = Vec2 { x: 0.0, y: -1.0 };
70    const WEST_REF_VEC: Vec2<f32> = Vec2 { x: -1.0, y: 0.0 };
71
72    /// When docking, the side to use depends on the angle the airship is
73    /// approaching the dock from, and the platform of the airship dock that
74    /// the airship is docking at.
75    /// For example, when docking at the North Platform:
76    ///
77    /// | From the          |  Docking Side |
78    /// |:----------------- |:--------------|
79    /// | West              |  Starboard    |
80    /// | Northwest         |  Starboard    |
81    /// | North             |  Starboard    |
82    /// | Northeast         |  Port         |
83    /// | East              |  Port         |
84    /// | Southeast         |  Port         |
85    /// | South             |  Port         |
86    /// | Southwest         |  Starboard    |
87    pub fn from_dir_to_platform(dir: &Vec2<f32>, platform: &AirshipDockPlatform) -> Self {
88        // get the reference vector and precompute whether to flip the angle based on
89        // the dir input.
90        let (ref_vec, negate_angle) = match platform {
91            AirshipDockPlatform::NorthPlatform => (&AirshipDockingSide::NORTH_REF_VEC, dir.x < 0.0),
92            AirshipDockPlatform::EastPlatform => (&AirshipDockingSide::EAST_REF_VEC, dir.y > 0.0),
93            AirshipDockPlatform::SouthPlatform => (&AirshipDockingSide::SOUTH_REF_VEC, dir.x > 0.0),
94            AirshipDockPlatform::WestPlatform => (&AirshipDockingSide::WEST_REF_VEC, dir.y < 0.0),
95        };
96        let mut angle = dir.angle_between(*ref_vec).to_degrees();
97        if negate_angle {
98            angle = -angle;
99        }
100        match angle as i32 {
101            -360..=0 => AirshipDockingSide::Port,
102            _ => AirshipDockingSide::Starboard,
103        }
104    }
105}
106
107/// Information needed for an airship to travel to and dock at an AirshipDock
108/// plot.
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct AirshipDockingApproach {
111    /// The position of the airship when docked.
112    /// This is different from dock_pos because the airship is offset to align
113    /// the ramp with the dock.
114    pub airship_pos: Vec3<f32>,
115    /// The direction the airship is facing when docked.
116    pub airship_direction: Dir,
117    /// Then center of the AirshipDock Plot.
118    pub dock_center: Vec2<f32>,
119    /// The height above terrain the airship cruises at.
120    pub height: f32,
121    /// The midpoint of the cruise phase of flight.
122    pub midpoint: Vec2<f32>,
123    /// The end point of the cruise phase of flight.
124    pub approach_transition_pos: Vec2<f32>,
125    /// There are ramps on both the port and starboard sides of the airship.
126    /// This gives the side that the airship will dock on.
127    pub side: AirshipDockingSide,
128    /// The site where the airship will be docked at the end of the
129    /// approach.
130    pub site_id: Id<Site>,
131}
132
133/// The docking postions at an AirshipDock plot.
134#[derive(Clone, Debug)]
135pub struct AirshipDockPositions {
136    /// The center of the AirshipDock plot. From the world generation code.
137    pub center: Vec2<f32>,
138    /// The docking positions for the airship, derived from the
139    /// positions calculated in the world generation code.
140    pub docking_positions: Vec<AirshipDockingPosition>,
141    /// The id of the Site where the AirshipDock is located.
142    pub site_id: Id<site::Site>,
143}
144
145/// The flight phases of an airship.
146#[derive(Debug, Copy, Clone, PartialEq, Default)]
147#[repr(usize)]
148pub enum AirshipFlightPhase {
149    DepartureCruise,
150    ApproachCruise,
151    Transition,
152    Descent,
153    #[default]
154    Docked,
155    Ascent,
156}
157
158impl std::fmt::Display for AirshipFlightPhase {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            AirshipFlightPhase::DepartureCruise => write!(f, "DepartureCruise"),
162            AirshipFlightPhase::ApproachCruise => write!(f, "ApproachCruise"),
163            AirshipFlightPhase::Transition => write!(f, "Transition"),
164            AirshipFlightPhase::Descent => write!(f, "Descent"),
165            AirshipFlightPhase::Docked => write!(f, "Docked"),
166            AirshipFlightPhase::Ascent => write!(f, "Ascent"),
167        }
168    }
169}
170
171/// One flight phase of an airship route leg.
172/// The position is the destination (or only) location for the segment.
173#[derive(Clone, Default, Debug)]
174pub struct RouteLegSegment {
175    /// Flight phase describes what the airship is doing
176    /// and is used in the NPC logic to define how the airship moves to the
177    /// position.
178    pub flight_phase: AirshipFlightPhase,
179    /// The starting (or only) position for the segment.
180    pub from_world_pos: Vec2<f32>,
181    /// The destination (or only) position for the segment.
182    pub to_world_pos: Vec2<f32>,
183    /// The distance covered in world blocks.
184    pub distance: f32,
185    /// How long it's supposed to take to cover the distance in seconds.
186    pub duration: f32,
187    /// The time at which the airship is supposed to arrive at the destination,
188    /// or the end of the docking phase. This is the cumulative time for the
189    /// entire route including all previous leg segments.
190    pub route_time: f64,
191}
192
193/// One leg of an airship route.
194/// The leg starts when the airship leaves the docking area at the end of the
195/// ascent phase and ends at the end of the ascent phase at the docking
196/// destination. Leg segments are:
197/// - Departure Cruise (DC) from the end of the previous ascent to the leg
198///   midpoint.
199/// - Approach Cruise (AC) from the leg midpoint to the transition start.
200/// - Transition (T) from the transition pos to above the dock.
201/// - Descent (D) to the docking position.
202/// - Parked/Docked (P) at the dock.
203/// - Ascent (A) back to cruising height above the dock.
204#[derive(Clone, Default, Debug)]
205pub struct AirshipRouteLeg {
206    /// The index of the destination in Airships::docking_positions.
207    pub dest_index: usize,
208    /// The assigned docking platform at the destination dock for this leg.
209    pub platform: AirshipDockPlatform,
210    /// The leg segments.
211    pub segments: [RouteLegSegment; 6],
212}
213
214/// An airship route is a series of legs that form a continuous loop.
215/// Each leg goes from one airship docking site to another.
216#[derive(Debug, Clone)]
217pub struct AirshipRoute {
218    pub legs: Vec<AirshipRouteLeg>,
219    pub total_time: f64,
220    pub airship_time_spacing: f64,
221    pub cruising_height: f32,
222    pub spawning_locations: Vec<AirshipSpawningLocation>,
223}
224
225/// Data for airship operations. This is generated world data.
226#[derive(Clone, Default)]
227pub struct Airships {
228    /// The docking positions for all airship docks in the world.
229    pub airship_docks: Vec<AirshipDockPositions>,
230    /// The routes flown by the collective airships in the world.
231    pub routes: Vec<AirshipRoute>,
232    /// The speed of simulated airships (and the nominal speed of loaded
233    /// airships) in world blocks per second.
234    pub nominal_speed: f32,
235}
236
237/// Information needed for placing an airship in the world when the world is
238/// generated (each time the server starts).
239#[derive(Debug, Clone)]
240pub struct AirshipSpawningLocation {
241    /// The flight phase the airship is in when spawned.
242    pub flight_phase: AirshipFlightPhase,
243    /// The 2D position of the airship when spawned.
244    pub pos: Vec2<f32>,
245    /// The direction the airship is facing when spawned.
246    pub dir: Vec2<f32>,
247    /// For cruise and transition phases, the height above terrain.
248    /// For descent, docked, and ascent phases, the actual z position.
249    pub height: f32,
250    /// The index of the route the airship is flying.
251    pub route_index: usize,
252    /// The index of the leg in the route the airship is flying.
253    pub leg_index: usize,
254    /// The effective route time when the airship is spawned.
255    /// This will be less than the route time for the end
256    /// of the phase the airship is in.
257    pub spawn_route_time: f64,
258}
259
260// Internal data structures
261
262impl AirshipDockPositions {
263    fn from_plot_meta(
264        first_id: u32,
265        center: Vec2<i32>,
266        docking_positions: &[Vec3<i32>],
267        site_id: Id<site::Site>,
268    ) -> Self {
269        let mut dock_pos_id = first_id;
270        Self {
271            center: center.map(|i| i as f32),
272            docking_positions: docking_positions
273                .iter()
274                .map(|pos: &Vec3<i32>| {
275                    let docking_position =
276                        AirshipDockingPosition(dock_pos_id, pos.map(|i| i as f32));
277                    dock_pos_id += 1;
278                    docking_position
279                })
280                .collect(),
281            site_id,
282        }
283    }
284
285    /// Get the docking position that matches the given platform.
286    fn docking_position(&self, platform: AirshipDockPlatform) -> Vec3<f32> {
287        self.docking_positions
288            .iter()
289            .find_map(|&docking_position| {
290                // The docking position is the one that matches the platform.
291                // The platform is determined by the direction of the docking position
292                // relative to the center of the dock.
293                let docking_position_platform =
294                    AirshipDockPlatform::from_dir(docking_position.1.xy() - self.center);
295                if docking_position_platform == platform {
296                    Some(docking_position.1)
297                } else {
298                    None
299                }
300            })
301            .unwrap_or_else(|| {
302                // If no docking position is found, return the dock center.
303                self.center.with_z(1000.0)
304            })
305    }
306}
307
308// These are used in AirshipDockPlatform::choices_from_dir
309
310static SWEN_PLATFORMS: [AirshipDockPlatform; 4] = [
311    AirshipDockPlatform::SouthPlatform,
312    AirshipDockPlatform::WestPlatform,
313    AirshipDockPlatform::EastPlatform,
314    AirshipDockPlatform::NorthPlatform,
315];
316
317static SEWN_PLATFORMS: [AirshipDockPlatform; 4] = [
318    AirshipDockPlatform::SouthPlatform,
319    AirshipDockPlatform::EastPlatform,
320    AirshipDockPlatform::WestPlatform,
321    AirshipDockPlatform::NorthPlatform,
322];
323
324static WNSE_PLATFORMS: [AirshipDockPlatform; 4] = [
325    AirshipDockPlatform::WestPlatform,
326    AirshipDockPlatform::NorthPlatform,
327    AirshipDockPlatform::SouthPlatform,
328    AirshipDockPlatform::EastPlatform,
329];
330
331static WSNE_PLATFORMS: [AirshipDockPlatform; 4] = [
332    AirshipDockPlatform::WestPlatform,
333    AirshipDockPlatform::SouthPlatform,
334    AirshipDockPlatform::NorthPlatform,
335    AirshipDockPlatform::EastPlatform,
336];
337
338static NEWS_PLATFORMS: [AirshipDockPlatform; 4] = [
339    AirshipDockPlatform::NorthPlatform,
340    AirshipDockPlatform::EastPlatform,
341    AirshipDockPlatform::WestPlatform,
342    AirshipDockPlatform::SouthPlatform,
343];
344
345static NWES_PLATFORMS: [AirshipDockPlatform; 4] = [
346    AirshipDockPlatform::NorthPlatform,
347    AirshipDockPlatform::WestPlatform,
348    AirshipDockPlatform::EastPlatform,
349    AirshipDockPlatform::SouthPlatform,
350];
351
352static ESNW_PLATFORMS: [AirshipDockPlatform; 4] = [
353    AirshipDockPlatform::EastPlatform,
354    AirshipDockPlatform::SouthPlatform,
355    AirshipDockPlatform::NorthPlatform,
356    AirshipDockPlatform::WestPlatform,
357];
358
359static ENSW_PLATFORMS: [AirshipDockPlatform; 4] = [
360    AirshipDockPlatform::EastPlatform,
361    AirshipDockPlatform::NorthPlatform,
362    AirshipDockPlatform::SouthPlatform,
363    AirshipDockPlatform::WestPlatform,
364];
365
366/// The docking platforms used on each leg of the airship route segments is
367/// determined when the routes are generated. Route segments are continuous
368/// loops that are deconflicted by using only one docking platform for any given
369/// leg of a route segment. Since there are four docking platforms per airship
370/// dock, there are at most four route segments passing through a given airship
371/// dock. The docking platforms are also optimized so that on the incoming leg
372/// of a route segment, the airship uses the docking platform that is closest to
373/// the arrival direction (if possible), while still using only one docking
374/// platform per route segment leg.
375impl AirshipDockPlatform {
376    /// Get the preferred docking platform based on the direction vector.
377    pub fn from_dir(dir: Vec2<f32>) -> Self {
378        if let Some(dir) = dir.try_normalized() {
379            let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
380            if dir.x < 0.0 {
381                angle = -angle;
382            }
383            match angle as i32 {
384                -360..=-135 => AirshipDockPlatform::SouthPlatform,
385                -134..=-45 => AirshipDockPlatform::WestPlatform,
386                -44..=45 => AirshipDockPlatform::NorthPlatform,
387                46..=135 => AirshipDockPlatform::EastPlatform,
388                136..=360 => AirshipDockPlatform::SouthPlatform,
389                _ => AirshipDockPlatform::NorthPlatform, // should never happen
390            }
391        } else {
392            AirshipDockPlatform::NorthPlatform // default value, should never happen
393        }
394    }
395
396    /// Get the platform choices in order of preference based on the direction
397    /// vector. The first choice is always the most direct plaform given the
398    /// approach direction. Then, the next two choices are the platforms for
399    /// the cardinal directions on each side of the approach direction, and
400    /// the last choice is the platform on the opposite side of the dock.
401    /// The return value is one of the ABCD_PLATFORMS arrays defined above.
402    pub fn choices_from_dir(dir: Vec2<f32>) -> &'static [AirshipDockPlatform] {
403        if let Some(dir) = dir.try_normalized() {
404            let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
405            if dir.x < 0.0 {
406                angle = -angle;
407            }
408            // This code works similar to the Direction enum in the common crate.
409            // Angle between produces the smallest angle between two vectors,
410            // so when dir.x is negative, we force the angle to be negative.
411            // 0 or 360 is North. It is assumed that the angle ranges from -360 to 360
412            // degrees even though angles less than -180 or greater than 180
413            // should never be seen.
414            match angle as i32 {
415                -360..=-135 => {
416                    // primary is SouthPlatform
417                    // As a fallback (for when the south platform is already claimed),
418                    // if the direction is more towards the west, use the west platform,
419                    // and if the direction is more towards the east, use the east platform.
420                    // The north platform is always the last resort. All fallback blocks
421                    // below work similarly.
422                    if angle as i32 > -180 {
423                        &SWEN_PLATFORMS
424                    } else {
425                        &SEWN_PLATFORMS
426                    }
427                },
428                -134..=-45 => {
429                    // primary is WestPlatform
430                    if angle as i32 > -90 {
431                        &WNSE_PLATFORMS
432                    } else {
433                        &WSNE_PLATFORMS
434                    }
435                },
436                -44..=45 => {
437                    // primary is NorthPlatform
438                    if angle as i32 > 0 {
439                        &NEWS_PLATFORMS
440                    } else {
441                        &NWES_PLATFORMS
442                    }
443                },
444                46..=135 => {
445                    // primary is EastPlatform
446                    if angle as i32 > 90 {
447                        &ESNW_PLATFORMS
448                    } else {
449                        &ENSW_PLATFORMS
450                    }
451                },
452                136..=360 => {
453                    // primary is SouthPlatform
454                    if angle as i32 > 180 {
455                        &SWEN_PLATFORMS
456                    } else {
457                        &SEWN_PLATFORMS
458                    }
459                },
460                _ => &SEWN_PLATFORMS,
461            }
462        } else {
463            &SEWN_PLATFORMS
464        }
465    }
466
467    /// Get the direction vector that the airship would be facing when docked.
468    fn airship_dir_for_side(&self, side: AirshipDockingSide) -> Dir {
469        match self {
470            AirshipDockPlatform::NorthPlatform => match side {
471                AirshipDockingSide::Starboard => Dir::new(Vec2::unit_x().with_z(0.0)),
472                AirshipDockingSide::Port => Dir::new(-Vec2::unit_x().with_z(0.0)),
473            },
474            AirshipDockPlatform::EastPlatform => match side {
475                AirshipDockingSide::Starboard => Dir::new(-Vec2::unit_y().with_z(0.0)),
476                AirshipDockingSide::Port => Dir::new(Vec2::unit_y().with_z(0.0)),
477            },
478            AirshipDockPlatform::SouthPlatform => match side {
479                AirshipDockingSide::Starboard => Dir::new(-Vec2::unit_x().with_z(0.0)),
480                AirshipDockingSide::Port => Dir::new(Vec2::unit_x().with_z(0.0)),
481            },
482            AirshipDockPlatform::WestPlatform => match side {
483                AirshipDockingSide::Starboard => Dir::new(Vec2::unit_y().with_z(0.0)),
484                AirshipDockingSide::Port => Dir::new(-Vec2::unit_y().with_z(0.0)),
485            },
486        }
487    }
488}
489
490/// A node on the triangulation of the world docking sites, with
491/// data on the nodes that are connected to it.
492#[derive(Clone, Debug, Default, PartialEq)]
493pub struct DockNode {
494    /// An index into the array of all nodes in the graph.
495    pub node_id: usize,
496    /// True if the node is on the outer hull (convex hull) of the
497    /// triangulation.
498    pub on_hull: bool,
499    /// The nodes that are connected to this node.
500    pub connected: Vec<usize>,
501}
502
503impl Airships {
504    /// The duration of the ascent flight phase.
505    pub const AIRSHIP_ASCENT_DURATION: f64 = 30.0;
506    /// The duration of the descent flight phase.
507    pub const AIRSHIP_DESCENT_DURATION: f64 = 30.0;
508    /// The duration of the docked phase.
509    pub const AIRSHIP_DOCKING_DURATION: f64 = 60.0;
510    /// The time spacing between airships on the same route.
511    pub const AIRSHIP_TIME_SPACING: f64 = 240.0;
512    /// The Z offset between the docking alignment point and the AirshipDock
513    /// plot docking position.
514    const AIRSHIP_TO_DOCK_Z_OFFSET: f32 = -3.0;
515    /// The ratio of the speed used when transitioning from cruising to docking
516    /// as a percentage of the nominal airship speed.
517    pub const AIRSHIP_TRANSITION_SPEED_RATIO: f32 = 0.75;
518    /// The cruising height varies by route index and there can be only four
519    /// routes.
520    pub const CRUISE_HEIGHTS: [f32; 4] = [400.0, 475.0, 550.0, 625.0];
521    /// The distance from the docking position where the airship starts the
522    /// transition flight phase.
523    const DOCKING_TRANSITION_OFFSET: f32 = 175.0;
524    /// The vector from the dock alignment point when the airship is docked on
525    /// the port side.
526    const DOCK_ALIGN_POS_PORT: Vec2<f32> =
527        Vec2::new(Airships::DOCK_ALIGN_X, -Airships::DOCK_ALIGN_Y);
528    /// The vector from the dock alignment point on the airship when the airship
529    /// is docked on the starboard side.
530    const DOCK_ALIGN_POS_STARBOARD: Vec2<f32> =
531        Vec2::new(-Airships::DOCK_ALIGN_X, -Airships::DOCK_ALIGN_Y);
532    // TODO: These alignment offsets are specific to the airship model. If new
533    // models are added, a more generic way to determine the alignment offsets
534    // should be used.
535    /// The absolute offset from the airship's position to the docking alignment
536    /// point on the X axis. The airship is assumed to be facing positive Y.
537    const DOCK_ALIGN_X: f32 = 18.0;
538    /// The offset from the airship's position to the docking alignment point on
539    /// the Y axis. The airship is assumed to be facing positive Y.
540    /// This is positive if the docking alignment point is in front of the
541    /// airship's center position.
542    const DOCK_ALIGN_Y: f32 = 1.0;
543    /// The minimum distance from the route leg midpoint to the world
544    /// boundaries.
545    const ROUTE_LEG_MIDPOINT_MARGIN: f32 = 200.0;
546    /// The angle for calculating the route leg midpoint.
547    const ROUTE_LEG_MIDPOINT_OFFSET_RADIANS: f32 = 0.087266;
548
549    // 5 degrees
550
551    #[inline(always)]
552    pub fn docking_duration() -> f32 { Airships::AIRSHIP_DOCKING_DURATION as f32 }
553
554    /// Get all the airship docking positions from the world sites.
555    fn all_airshipdock_positions(sites: &Store<Site>) -> Vec<AirshipDockPositions> {
556        let mut dock_pos_id = 0;
557        sites
558            .iter()
559            .flat_map(|(site_id, site)| {
560                site.plots().flat_map(move |plot| {
561                    if let Some(PlotKindMeta::AirshipDock {
562                        center,
563                        docking_positions,
564                        ..
565                    }) = plot.kind().meta()
566                    {
567                        Some((center, docking_positions, site_id))
568                    } else {
569                        None
570                    }
571                })
572            })
573            .map(|(center, docking_positions, site_id)| {
574                let positions = AirshipDockPositions::from_plot_meta(
575                    dock_pos_id,
576                    center,
577                    docking_positions,
578                    site_id,
579                );
580
581                dock_pos_id += positions.docking_positions.len() as u32;
582                positions
583            })
584            .collect::<Vec<_>>()
585    }
586
587    /// Convienence function that returns the next route leg accounting for wrap
588    /// around.
589    pub fn increment_route_leg(&self, route_index: usize, leg_index: usize) -> usize {
590        if route_index >= self.routes.len() {
591            error!("Invalid route index: {}", route_index);
592            return 0;
593        }
594        (leg_index + 1) % self.routes[route_index].legs.len()
595    }
596
597    /// Convienence function that returns the previous route leg accounting for
598    /// wrap around.
599    pub fn decrement_route_leg(&self, route_index: usize, leg_index: usize) -> usize {
600        if route_index >= self.routes.len() {
601            error!("Invalid route index: {}", route_index);
602            return 0;
603        }
604        if leg_index > 0 {
605            leg_index - 1
606        } else {
607            self.routes[route_index].legs.len() - 1
608        }
609    }
610
611    /// Convienence function returning the number of routes.
612    pub fn route_count(&self) -> usize { self.routes.len() }
613
614    /// Safe function to get the number of AirshipDock sites on a route.
615    pub fn docking_site_count_for_route(&self, route_index: usize) -> usize {
616        if route_index >= self.routes.len() {
617            error!("Invalid route index: {}", route_index);
618            return 0;
619        }
620        self.routes[route_index].legs.len()
621    }
622
623    /// Calculate the legs for each route.
624    /// A docking platform is assigned for each leg of each route. Each route
625    /// in the route_segments argument is a series (Vec) of docking node indices
626    /// on the docking site graph. This function loops over the routes docking
627    /// nodes and assigns a docking platform based on the approach direction
628    /// to each dock node while making sure that no docking platform is used
629    /// more than once (globally, over all routes). It then calculates the
630    /// leg segments (flight phases) for each leg of each route. The output
631    /// is a Vec of up to four routes, each of which is a closed loop of
632    /// route legs.
633    fn create_route_legs(
634        &mut self,
635        route_segments: &[Vec<usize>],
636        dock_locations: &[Vec2<f32>],
637        map_size_lg: &MapSizeLg,
638    ) -> Vec<AirshipRoute> {
639        let mut incoming_edges = DHashMap::default();
640        for segment in route_segments.iter() {
641            if segment.len() < 3 {
642                continue;
643            }
644            let mut prev_node_id = segment[0];
645            segment.iter().skip(1).for_each(|&node_id| {
646                incoming_edges
647                    .entry(node_id)
648                    .or_insert_with(Vec::new)
649                    .push(prev_node_id);
650                prev_node_id = node_id;
651            });
652        }
653
654        let mut leg_platforms = DHashMap::default();
655
656        incoming_edges.iter().for_each(|(node_id, edges)| {
657            let dock_location = dock_locations[*node_id];
658            let mut used_platforms = DHashSet::default();
659            for origin in edges {
660                let origin_location = dock_locations[*origin];
661                // Determine the platform to dock using the direction from the dock location
662                // to the origin location
663                let rev_approach_dir = origin_location - dock_location;
664                let docking_platforms = AirshipDockPlatform::choices_from_dir(rev_approach_dir);
665                let docking_platform = docking_platforms
666                    .iter()
667                    .find(|&platform| !used_platforms.contains(platform))
668                    .copied()
669                    .unwrap_or(AirshipDockPlatform::NorthPlatform);
670                leg_platforms.insert((*origin, *node_id), docking_platform);
671                used_platforms.insert(docking_platform);
672            }
673        });
674
675        #[cfg(debug_assertions)]
676        {
677            debug_airships!(4, "Route segments: {:?}", route_segments);
678            debug_airships!(4, "Leg platforms: {:?}", leg_platforms);
679        }
680
681        self.nominal_speed = common::comp::ship::Body::DefaultAirship.get_speed();
682        debug_airships!(4, "Nominal speed {}", self.nominal_speed);
683
684        // The incoming edges control the docking platforms used for each leg of the
685        // route. The outgoing platform for leg i must match the incoming
686        // platform for leg i-1. For the first leg, get the 'from' platform from
687        // the last pair of nodes in the segment.
688        let mut routes = Vec::new();
689        route_segments
690            .iter()
691            .enumerate()
692            .for_each(|(route_index, segment)| {
693                assert!(
694                    segment.len() > 2,
695                    "Segments must have at least two nodes and they must wrap around."
696                );
697                let mut route_legs = Vec::new();
698                let mut route_time = 0.0;
699                let leg_start = &segment[segment.len() - 2..];
700                for leg_index in 0..segment.len() - 1 {
701                    let from_node = segment[leg_index];
702                    let to_node = segment[leg_index + 1];
703                    if leg_index == 0 {
704                        assert!(
705                            from_node == leg_start[1],
706                            "The 'previous' leg's 'to' node must match the current leg's 'from' \
707                             node."
708                        );
709                    }
710                    let to_platform = leg_platforms.get(&(from_node, to_node)).copied().unwrap_or(
711                        AirshipDockPlatform::from_dir(
712                            dock_locations[from_node] - dock_locations[to_node],
713                        ),
714                    );
715                    let dest_dock_positions = &self.airship_docks[to_node];
716                    let from_dock_positions = &self.airship_docks[from_node];
717                    let (midpoint, approach_transition_pos) = self.approach_waypoints(
718                        &from_dock_positions.center,
719                        dest_dock_positions,
720                        to_platform,
721                        map_size_lg,
722                    );
723                    let dest_dock_pos = dest_dock_positions.docking_position(to_platform).xy();
724
725                    // depature cruise (DC) from the end of the previous ascent to the leg midpoint.
726                    // The departure platform is not known here, so just use the dock center.
727                    // distance is from the departure dock center to the midpoint.
728                    // duration is distance / nominal speed
729                    let dc_dist = from_dock_positions
730                        .center
731                        .as_::<f64>()
732                        .distance(midpoint.as_());
733                    let dc_dur = dc_dist / self.nominal_speed as f64;
734                    let dc_completion_time = route_time + dc_dur;
735
736                    // approach cruise (AC) from the leg midpoint to the transition start.
737                    // distance is from the midpoint to approach_transition_pos.
738                    // duration is distance / nominal speed
739                    let ac_dist = midpoint
740                        .as_::<f64>()
741                        .distance(approach_transition_pos.as_());
742                    let ac_dur = ac_dist / self.nominal_speed as f64;
743                    let ac_completion_time = dc_completion_time + ac_dur;
744
745                    // transition (T) from approach_transition_pos to above the dock.
746                    // distance is from approach_transition_pos to the dock position.
747                    // duration is distance / (nominal speed * AIRSHIP_TRANSITION_SPEED_RATIO)
748                    let t_dist = approach_transition_pos
749                        .as_::<f64>()
750                        .distance(dest_dock_pos.as_());
751                    let t_dur = t_dist
752                        / (self.nominal_speed * Airships::AIRSHIP_TRANSITION_SPEED_RATIO) as f64;
753                    let t_completion_time = ac_completion_time + t_dur;
754
755                    // descent (D) to the docking position.
756                    // distance is 0 (no x,y movement)
757                    // duration is fixed at AIRSHIP_DESCENT_DURATION
758                    let d_completion_time = t_completion_time + Airships::AIRSHIP_DESCENT_DURATION;
759
760                    // parked/docked (P) at the dock.
761                    // distance is 0
762                    // duration is fixed at AIRSHIP_DOCKING_DURATION
763                    let p_completion_time = d_completion_time + Airships::AIRSHIP_DOCKING_DURATION;
764
765                    // ascent (A) back to cruising height above the dock.
766                    // distance is 0
767                    // duration is fixed at AIRSHIP_ASCENT_DURATION
768                    let a_completion_time = p_completion_time + Airships::AIRSHIP_ASCENT_DURATION;
769
770                    route_legs.push(AirshipRouteLeg {
771                        dest_index: to_node,
772                        platform: to_platform,
773                        segments: [
774                            RouteLegSegment {
775                                flight_phase: AirshipFlightPhase::DepartureCruise,
776                                from_world_pos: from_dock_positions.center,
777                                to_world_pos: midpoint,
778                                distance: from_dock_positions.center.distance(midpoint),
779                                duration: dc_dur as f32,
780                                route_time: dc_completion_time,
781                            },
782                            RouteLegSegment {
783                                flight_phase: AirshipFlightPhase::ApproachCruise,
784                                from_world_pos: midpoint,
785                                to_world_pos: approach_transition_pos,
786                                distance: midpoint.distance(approach_transition_pos),
787                                duration: ac_dur as f32,
788                                route_time: ac_completion_time,
789                            },
790                            RouteLegSegment {
791                                flight_phase: AirshipFlightPhase::Transition,
792                                from_world_pos: approach_transition_pos,
793                                to_world_pos: dest_dock_pos,
794                                distance: approach_transition_pos.distance(dest_dock_pos),
795                                duration: t_dur as f32,
796                                route_time: t_completion_time,
797                            },
798                            RouteLegSegment {
799                                flight_phase: AirshipFlightPhase::Descent,
800                                from_world_pos: dest_dock_pos,
801                                to_world_pos: dest_dock_pos,
802                                distance: 0.0,
803                                duration: Airships::AIRSHIP_DESCENT_DURATION as f32,
804                                route_time: d_completion_time,
805                            },
806                            RouteLegSegment {
807                                flight_phase: AirshipFlightPhase::Docked,
808                                from_world_pos: dest_dock_pos,
809                                to_world_pos: dest_dock_pos,
810                                distance: 0.0,
811                                duration: Airships::AIRSHIP_DOCKING_DURATION as f32,
812                                route_time: p_completion_time,
813                            },
814                            RouteLegSegment {
815                                flight_phase: AirshipFlightPhase::Ascent,
816                                from_world_pos: dest_dock_pos,
817                                to_world_pos: dest_dock_pos,
818                                distance: 0.0,
819                                duration: Airships::AIRSHIP_ASCENT_DURATION as f32,
820                                route_time: a_completion_time,
821                            },
822                        ],
823                    });
824                    route_time = a_completion_time;
825                }
826                let spawning_location_count =
827                    (route_time / Airships::AIRSHIP_TIME_SPACING).floor() as usize;
828                let route_sep = (route_time / spawning_location_count as f64).floor();
829                debug_airships!(
830                    4,
831                    "Route {} total_time: {}, spawning_location_count: {}, route_sep: {}",
832                    route_index,
833                    route_time,
834                    spawning_location_count,
835                    route_sep
836                );
837
838                routes.push(AirshipRoute {
839                    legs: route_legs,
840                    total_time: route_time,
841                    airship_time_spacing: route_sep,
842                    cruising_height: Airships::CRUISE_HEIGHTS
843                        [route_index % Airships::CRUISE_HEIGHTS.len()],
844                    spawning_locations: Vec::new(),
845                });
846            });
847        #[cfg(debug_assertions)]
848        {
849            routes.iter().enumerate().for_each(|(i, route)| {
850                debug_airships!(4, "Route {} legs: {}", i, route.legs.len());
851                route.legs.iter().enumerate().for_each(|(j, leg)| {
852                    debug_airships!(4, "  Leg {}: dest_index: {}", j, leg.dest_index);
853                    leg.segments.iter().enumerate().for_each(|(k, seg)| {
854                        debug_airships!(
855                            4,
856                            "route {} leg {} segment {}: phase: {:?}, from: {}, to: {}, dist: {}, \
857                             route_time: {}",
858                            i,
859                            j,
860                            k,
861                            seg.flight_phase,
862                            seg.from_world_pos,
863                            seg.to_world_pos,
864                            seg.distance,
865                            seg.route_time
866                        );
867                    });
868                });
869            });
870        }
871        routes
872    }
873
874    pub fn calculate_spawning_locations(&mut self) {
875        // Spawn airships according to route time so that they are spaced out evenly in
876        // time.
877        for (route_index, route) in self.routes.iter_mut().enumerate() {
878            let spawn_time_limit = route.total_time - route.airship_time_spacing;
879            let mut next_spawn_time = 0.0;
880            let mut prev_seg_route_time = 0.0;
881            let nominal_speed = common::comp::ship::Body::DefaultAirship.get_speed();
882            let mut spawning_locations = Vec::new();
883            // if route.legs.is_empty() || route.legs[0].segments.is_empty() {
884            //     continue;
885            // }
886            //let mut prev_leg_segment = &route.legs[route.legs.len() -
887            // 1].segments[route.legs[route.legs.len() - 1].segments.len() - 1];
888            for (leg_index, leg) in route.legs.iter().enumerate() {
889                let leg_count = route.legs.len();
890                let from_route_leg = &route.legs[(leg_index + leg_count - 1) % leg_count];
891                let dest_dock_positions = &self.airship_docks[leg.dest_index];
892                let from_dock_positions = &self.airship_docks[from_route_leg.dest_index];
893
894                for seg in leg.segments.iter() {
895                    while next_spawn_time <= seg.route_time && next_spawn_time <= spawn_time_limit {
896                        // spawn an airship on this leg segment at time next_spawn_time
897                        // The spawning location depends on the flight phase.
898                        // DepartureCruise:
899                        //     dist = (next_spawn_time - prev_leg.segments[5].route_time) *
900                        // nominal_speed     pos = (midpoint - previous leg
901                        // docking position).normalized() * dist + previous leg docking position
902                        // ApproachCruise:
903                        //     dist = (next_spawn_time - leg.segments[0].route_time) * nominal_speed
904                        //     pos = (transition_pos - midpoint).normalized() * dist + midpoint
905                        // Transition:
906                        //     dist = (next_spawn_time - leg.segments[1].route_time)
907                        //     pos = (dock_pos - transition_pos).normalized() * dist +
908                        // transition_pos Descent:
909                        //     pos = dock_pos
910                        // Docked:
911                        //     pos = dock_pos
912                        // Ascent:
913                        //     pos = dock_pos
914                        match seg.flight_phase {
915                            AirshipFlightPhase::DepartureCruise => {
916                                let dist =
917                                    (next_spawn_time - prev_seg_route_time) as f32 * nominal_speed;
918                                let dir = seg.to_world_pos - seg.from_world_pos;
919                                let pos = seg.from_world_pos
920                                    + dir.try_normalized().unwrap_or(Vec2::zero()) * dist;
921                                spawning_locations.push(AirshipSpawningLocation {
922                                    flight_phase: seg.flight_phase,
923                                    pos,
924                                    dir: dir.try_normalized().unwrap_or(Vec2::unit_y()),
925                                    height: route.cruising_height,
926                                    route_index,
927                                    leg_index,
928                                    spawn_route_time: next_spawn_time,
929                                });
930                                debug_airships!(
931                                    4,
932                                    "route {} leg {} DepartureCruise prev_seg_route_time: {}, \
933                                     next_spawn_time: {}, seg.route_time: {}",
934                                    route_index,
935                                    leg_index,
936                                    prev_seg_route_time,
937                                    next_spawn_time,
938                                    seg.route_time
939                                );
940                                next_spawn_time += route.airship_time_spacing;
941                            },
942                            AirshipFlightPhase::ApproachCruise => {
943                                let dist =
944                                    (next_spawn_time - prev_seg_route_time) as f32 * nominal_speed;
945                                let dir = seg.to_world_pos - seg.from_world_pos;
946                                let pos = seg.from_world_pos
947                                    + dir.try_normalized().unwrap_or(Vec2::zero()) * dist;
948                                spawning_locations.push(AirshipSpawningLocation {
949                                    flight_phase: seg.flight_phase,
950                                    pos,
951                                    dir: dir.try_normalized().unwrap_or(Vec2::unit_y()),
952                                    height: route.cruising_height,
953                                    route_index,
954                                    leg_index,
955                                    spawn_route_time: next_spawn_time,
956                                });
957                                debug_airships!(
958                                    4,
959                                    "route {} leg {} ApproachCruise prev_seg_route_time: {}, \
960                                     next_spawn_time: {}, seg.route_time: {}",
961                                    route_index,
962                                    leg_index,
963                                    prev_seg_route_time,
964                                    next_spawn_time,
965                                    seg.route_time
966                                );
967                                next_spawn_time += route.airship_time_spacing;
968                            },
969                            AirshipFlightPhase::Transition => {
970                                let dist = (next_spawn_time - prev_seg_route_time) as f32
971                                    * nominal_speed
972                                    * Airships::AIRSHIP_TRANSITION_SPEED_RATIO;
973                                let dir = seg.to_world_pos - seg.from_world_pos;
974                                let pos = seg.from_world_pos
975                                    + dir.try_normalized().unwrap_or(Vec2::zero()) * dist;
976                                spawning_locations.push(AirshipSpawningLocation {
977                                    flight_phase: seg.flight_phase,
978                                    pos,
979                                    dir: dir.try_normalized().unwrap_or(Vec2::unit_y()),
980                                    height: route.cruising_height,
981                                    route_index,
982                                    leg_index,
983                                    spawn_route_time: next_spawn_time,
984                                });
985                                debug_airships!(
986                                    4,
987                                    "route {} leg {} Transition prev_seg_route_time: {}, \
988                                     next_spawn_time: {}, seg.route_time: {}",
989                                    route_index,
990                                    leg_index,
991                                    prev_seg_route_time,
992                                    next_spawn_time,
993                                    seg.route_time
994                                );
995                                next_spawn_time += route.airship_time_spacing;
996                            },
997                            AirshipFlightPhase::Descent => {
998                                let (airship_pos, airship_direction) =
999                                    Airships::docking_position_and_dir_for_route_and_leg(
1000                                        from_dock_positions,
1001                                        dest_dock_positions,
1002                                        leg.platform,
1003                                    );
1004                                let dt = next_spawn_time - prev_seg_route_time;
1005                                let dd = route.cruising_height - airship_pos.z;
1006                                let height = airship_pos.z
1007                                    + dd * (dt / Airships::AIRSHIP_DESCENT_DURATION) as f32;
1008                                let dir = airship_direction
1009                                    .vec()
1010                                    .xy()
1011                                    .try_normalized()
1012                                    .unwrap_or(Vec2::unit_y());
1013                                spawning_locations.push(AirshipSpawningLocation {
1014                                    flight_phase: seg.flight_phase,
1015                                    pos: seg.from_world_pos,
1016                                    dir,
1017                                    height,
1018                                    route_index,
1019                                    leg_index,
1020                                    spawn_route_time: next_spawn_time,
1021                                });
1022                                debug_airships!(
1023                                    4,
1024                                    "route {} leg {} Descent prev_seg_route_time: {}, \
1025                                     next_spawn_time: {}, seg.route_time: {}",
1026                                    route_index,
1027                                    leg_index,
1028                                    prev_seg_route_time,
1029                                    next_spawn_time,
1030                                    seg.route_time
1031                                );
1032                                next_spawn_time += route.airship_time_spacing;
1033                            },
1034                            AirshipFlightPhase::Docked => {
1035                                let (airship_pos, airship_direction) =
1036                                    Airships::docking_position_and_dir_for_route_and_leg(
1037                                        from_dock_positions,
1038                                        dest_dock_positions,
1039                                        leg.platform,
1040                                    );
1041                                let dir = airship_direction
1042                                    .vec()
1043                                    .xy()
1044                                    .try_normalized()
1045                                    .unwrap_or(Vec2::unit_y());
1046                                spawning_locations.push(AirshipSpawningLocation {
1047                                    flight_phase: seg.flight_phase,
1048                                    pos: seg.from_world_pos,
1049                                    dir,
1050                                    height: airship_pos.z,
1051                                    route_index,
1052                                    leg_index,
1053                                    spawn_route_time: next_spawn_time,
1054                                });
1055                                debug_airships!(
1056                                    4,
1057                                    "route {} leg {} Docked prev_seg_route_time: {}, \
1058                                     next_spawn_time: {}, seg.route_time: {}",
1059                                    route_index,
1060                                    leg_index,
1061                                    prev_seg_route_time,
1062                                    next_spawn_time,
1063                                    seg.route_time
1064                                );
1065                                next_spawn_time += route.airship_time_spacing;
1066                            },
1067                            AirshipFlightPhase::Ascent => {
1068                                let (airship_pos, airship_direction) =
1069                                    Airships::docking_position_and_dir_for_route_and_leg(
1070                                        from_dock_positions,
1071                                        dest_dock_positions,
1072                                        leg.platform,
1073                                    );
1074                                let dt = next_spawn_time - prev_seg_route_time;
1075                                let dd = route.cruising_height - airship_pos.z;
1076                                let height = airship_pos.z
1077                                    + dd * (dt / Airships::AIRSHIP_ASCENT_DURATION) as f32;
1078                                let dir = airship_direction
1079                                    .vec()
1080                                    .xy()
1081                                    .try_normalized()
1082                                    .unwrap_or(Vec2::unit_y());
1083                                spawning_locations.push(AirshipSpawningLocation {
1084                                    flight_phase: seg.flight_phase,
1085                                    pos: seg.from_world_pos,
1086                                    dir,
1087                                    height,
1088                                    route_index,
1089                                    leg_index,
1090                                    spawn_route_time: next_spawn_time,
1091                                });
1092                                debug_airships!(
1093                                    4,
1094                                    "route {} leg {} Ascent prev_seg_route_time: {}, \
1095                                     next_spawn_time: {}, seg.route_time: {}",
1096                                    route_index,
1097                                    leg_index,
1098                                    prev_seg_route_time,
1099                                    next_spawn_time,
1100                                    seg.route_time
1101                                );
1102                                next_spawn_time += route.airship_time_spacing;
1103                            },
1104                        }
1105                    }
1106                    prev_seg_route_time = seg.route_time;
1107                }
1108            }
1109            route.spawning_locations = spawning_locations;
1110        }
1111    }
1112
1113    /// Generates the airship routes.
1114    pub fn generate_airship_routes_inner(
1115        &mut self,
1116        map_size_lg: &MapSizeLg,
1117        seed: u32,
1118        _index: Option<&Index>,
1119        _sampler: Option<&WorldSim>,
1120        _map_image_path: Option<&str>,
1121    ) {
1122        let all_dock_points = self
1123            .airship_docks
1124            .iter()
1125            .map(|dock| Point {
1126                x: dock.center.x as f64,
1127                y: dock.center.y as f64,
1128            })
1129            .collect::<Vec<_>>();
1130        debug_airships!(4, "all_dock_points: {:?}", all_dock_points);
1131
1132        // Run the delaunay triangulation on the docking points.
1133        let triangulation = triangulate(&all_dock_points);
1134
1135        #[cfg(feature = "airship_maps")]
1136        save_airship_routes_triangulation(
1137            &triangulation,
1138            &all_dock_points,
1139            map_size_lg,
1140            seed,
1141            _index,
1142            _sampler,
1143            _map_image_path,
1144        );
1145
1146        // Docking positions are specified in world coordinates, not chunks.
1147        // Limit the max route leg length to 1000 chunks no matter the world size.
1148        let blocks_per_chunk = 1 << TERRAIN_CHUNK_BLOCKS_LG;
1149        let max_route_leg_length = 1000.0 * blocks_per_chunk as f32;
1150
1151        // eulerized_route_segments is fairly expensive as the number of docking sites
1152        // grows. Limit the number of iterations according to world size.
1153        // pow2     world size   iterations
1154        // 10       1024         50
1155        // 11       2048         22
1156        // 12       4096         10
1157        // 13       8192          2
1158        // Doing a least squares fit on the iterations gives the formula:
1159        // 3742931.0 * e.powf(-1.113823 * pow2)
1160        // 3742931.0 * 2.71828f32.powf(-1.113823 * pow2)
1161
1162        let pow2 = map_size_lg.vec().x;
1163        let max_iterations = (3742931.0 * std::f32::consts::E.powf(-1.113823 * pow2 as f32))
1164            .clamp(1.0, 100.0)
1165            .round() as usize;
1166
1167        if let Some((best_segments, _, _max_seg_len, _min_spread, _iteration)) = triangulation
1168            .eulerized_route_segments(
1169                &all_dock_points,
1170                max_iterations,
1171                max_route_leg_length as f64,
1172                seed,
1173            )
1174        {
1175            #[cfg(debug_assertions)]
1176            {
1177                debug_airships!(4, "Max segment length: {}", _max_seg_len);
1178                debug_airships!(4, "Min spread: {}", _min_spread);
1179                debug_airships!(4, "Iteration: {}", _iteration);
1180                debug_airships!(4, "Segments count:");
1181                let mut bidirectional_segments = Vec::new();
1182                best_segments.iter().enumerate().for_each(|segment| {
1183                    debug_airships!(4, "  {} : {}", segment.0, segment.1.len());
1184                    let seg_bidir = {
1185                        if segment.1.len() > 2 {
1186                            let slen = segment.1.len();
1187                            let mut bidir_found = false;
1188                            for index in 0..slen {
1189                                let back2 = segment.1[(index + slen - 2) % slen];
1190                                let curr = segment.1[index];
1191                                if curr == back2 {
1192                                    debug_airships!(
1193                                        4,
1194                                        "Segment {} bidir at index {}",
1195                                        segment.0,
1196                                        index
1197                                    );
1198                                    bidir_found = true;
1199                                }
1200                            }
1201                            bidir_found
1202                        } else {
1203                            false
1204                        }
1205                    };
1206                    bidirectional_segments.push(seg_bidir);
1207                });
1208                debug_airships!(4, "Best segments: {:?}", best_segments);
1209                debug_airships!(4, "Bi-dir: {:?}", bidirectional_segments);
1210                #[cfg(feature = "airship_maps")]
1211                if let Some(index) = _index
1212                    && let Some(world_sim) = _sampler
1213                    && let Err(e) = export_world_map(index, world_sim)
1214                {
1215                    eprintln!("Failed to export world map: {:?}", e);
1216                }
1217            }
1218
1219            self.routes = self.create_route_legs(
1220                &best_segments,
1221                all_dock_points
1222                    .iter()
1223                    .map(|p| Vec2::new(p.x as f32, p.y as f32))
1224                    .collect::<Vec<_>>()
1225                    .as_slice(),
1226                map_size_lg,
1227            );
1228
1229            // Calculate the spawning locations for airships on the routes.
1230            self.calculate_spawning_locations();
1231
1232            #[cfg(debug_assertions)]
1233            {
1234                self.routes.iter().enumerate().for_each(|(i, route)| {
1235                    debug_airships!(4, "Route {} spawning locations", i);
1236                    route.spawning_locations.iter().for_each(|loc| {
1237                        debug_airships!(
1238                            4,
1239                            "{} {:02} {:7.1}, {:7.1}, {} {}",
1240                            loc.route_index,
1241                            loc.leg_index,
1242                            loc.pos.x,
1243                            loc.pos.y,
1244                            loc.flight_phase,
1245                            loc.height,
1246                        );
1247                    });
1248                });
1249            }
1250
1251            #[cfg(feature = "airship_maps")]
1252            save_airship_route_segments(
1253                &self.routes,
1254                &all_dock_points,
1255                &self.airship_spawning_locations(),
1256                map_size_lg,
1257                seed,
1258                _index,
1259                _sampler,
1260                _map_image_path,
1261            );
1262        } else {
1263            eprintln!("Error - cannot eulerize the dock points.");
1264        }
1265    }
1266
1267    pub fn generate_airship_routes(&mut self, world_sim: &mut WorldSim, index: &Index) {
1268        self.airship_docks = Airships::all_airshipdock_positions(&index.sites);
1269
1270        self.generate_airship_routes_inner(
1271            &world_sim.map_size_lg(),
1272            index.seed,
1273            Some(index),
1274            Some(world_sim),
1275            None,
1276        );
1277    }
1278
1279    /// Compute the route midpoint and the transition point where the airship
1280    /// should stop the cruise flight phase and start the docking phase.
1281    /// ```text
1282    ///  F : From position
1283    ///  M : Midpoint
1284    ///  T : Transition point
1285    ///  D : Docking position
1286    ///  C : Center of the airship dock
1287    ///  X : Airship dock
1288    ///
1289    ///                            F
1290    ///                         •
1291    ///                      •
1292    ///                   M
1293    ///                  ∙
1294    ///                 ∙
1295    ///                T
1296    ///               ∙
1297    ///              ∙
1298    ///             D
1299    ///
1300    ///           XXXXX
1301    ///         XX     XX
1302    ///        X         X
1303    ///        X    C    X
1304    ///        X         X
1305    ///         XX     XX
1306    ///           XXXXX
1307    /// ```
1308    /// The midpoint is for route leg deconfliction and is where the airship
1309    /// makes a coarse correction to point at the destination. The
1310    /// transition point (T) between cruise flight and docking approach is
1311    /// on a line between the route leg midpoint (M) and the docking
1312    /// position (D), short of the docking position by
1313    /// Airships::DOCKING_TRANSITION_OFFSET blocks.
1314    ///
1315    /// # Arguments
1316    ///
1317    /// * `dock_index` - The airship dock index in airship_docks.
1318    /// * `route_index` - The index of the route (outer vector of
1319    ///   airships.routes). This is used to determine the cruise height.
1320    /// * `platform` - The platform on the airship dock where the airship is to
1321    ///   dock.
1322    /// * `from` - The position from which the airship is approaching the dock.
1323    /// # Returns
1324    /// The 2D position calculated with the Z coordinate set to the
1325    /// docking_position.z + cruise height.
1326    pub fn approach_waypoints(
1327        &self,
1328        from_dock_center: &Vec2<f32>,
1329        to_dock_positions: &AirshipDockPositions,
1330        platform: AirshipDockPlatform,
1331        map_size_lg: &MapSizeLg,
1332    ) -> (Vec2<f32>, Vec2<f32>) {
1333        // Get the route leg midpoint. This is the vector from the from_dock_position
1334        // to the to_dock_position rotated ROUTE_LEG_MID_POINT_OFFSET_RADIANS
1335        // at 1/2 the distance from the from_dock_position to the to_dock_position (so
1336        // not quite the exact midpoint but close enough).
1337        // Clamp midpoint so that it stays within the world bounds (with some margin).
1338        let blocks_per_chunk = 1 << TERRAIN_CHUNK_BLOCKS_LG;
1339        let world_blocks = map_size_lg.chunks().map(|u| u as f32) * blocks_per_chunk as f32;
1340        let midpoint = {
1341            // let from_pos = from_dock_positions.center;
1342            // let to_pos = dest_dock_positions.center;
1343            let dir = (to_dock_positions.center - from_dock_center).normalized();
1344            let mid_len = from_dock_center.distance(to_dock_positions.center) * 0.5;
1345            let mid_dir = dir.rotated_z(Airships::ROUTE_LEG_MIDPOINT_OFFSET_RADIANS);
1346            from_dock_center + mid_dir * mid_len
1347        }
1348        .clamped(
1349            Vec2::new(
1350                Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1351                Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1352            ),
1353            Vec2::new(
1354                world_blocks.x - Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1355                world_blocks.y - Airships::ROUTE_LEG_MIDPOINT_MARGIN,
1356            ),
1357        );
1358
1359        let transition_point = {
1360            // calculate the transition point looking from the destination position back to
1361            // the midpoint.
1362            let to_dir_rev = (midpoint - to_dock_positions.center).normalized();
1363            let docking_position = to_dock_positions.docking_position(platform);
1364            docking_position.xy() + to_dir_rev * Airships::DOCKING_TRANSITION_OFFSET
1365        };
1366
1367        (midpoint, transition_point)
1368    }
1369
1370    fn vec3_relative_eq(a: &vek::Vec3<f32>, b: &vek::Vec3<f32>, epsilon: f32) -> bool {
1371        (a.x - b.x).abs() < epsilon && (a.y - b.y).abs() < epsilon && (a.z - b.z).abs() < epsilon
1372    }
1373
1374    pub fn docking_position_and_dir_for_route_and_leg(
1375        from_dock_positions: &AirshipDockPositions,
1376        to_dock_positions: &AirshipDockPositions,
1377        platform: AirshipDockPlatform,
1378    ) -> (Vec3<f32>, Dir) {
1379        let docking_side = AirshipDockingSide::from_dir_to_platform(
1380            &(to_dock_positions.center - from_dock_positions.center),
1381            &platform,
1382        );
1383
1384        // get the airship position and direction when docked
1385        let (airship_pos, airship_direction) = Airships::airship_vec_for_docking_pos(
1386            to_dock_positions.docking_position(platform),
1387            to_dock_positions.center,
1388            Some(docking_side),
1389        );
1390        (airship_pos, airship_direction)
1391    }
1392
1393    pub fn approach_for_route_and_leg(
1394        &self,
1395        route_index: usize,
1396        leg_index: usize,
1397        map_size_lg: &MapSizeLg,
1398    ) -> AirshipDockingApproach {
1399        // Get the docking positions for the route and leg.
1400        let to_route_leg = &self.routes[route_index].legs[leg_index];
1401        let leg_count = self.routes[route_index].legs.len();
1402        let from_route_leg =
1403            &self.routes[route_index].legs[(leg_index + leg_count - 1) % leg_count];
1404        let dest_dock_positions = &self.airship_docks[to_route_leg.dest_index];
1405        let from_dock_positions = &self.airship_docks[from_route_leg.dest_index];
1406
1407        let docking_side = AirshipDockingSide::from_dir_to_platform(
1408            &(dest_dock_positions.center - from_dock_positions.center),
1409            &to_route_leg.platform,
1410        );
1411
1412        // get the airship position and direction when docked
1413        let (airship_pos, airship_direction) = Airships::airship_vec_for_docking_pos(
1414            dest_dock_positions.docking_position(to_route_leg.platform),
1415            dest_dock_positions.center,
1416            Some(docking_side),
1417        );
1418
1419        // get the leg midpoint and transition point
1420        let (midpoint, approach_transition_pos) = self.approach_waypoints(
1421            &from_dock_positions.center,
1422            dest_dock_positions,
1423            to_route_leg.platform,
1424            map_size_lg,
1425        );
1426
1427        AirshipDockingApproach {
1428            airship_pos,
1429            airship_direction,
1430            dock_center: dest_dock_positions.center,
1431            height: Airships::CRUISE_HEIGHTS[route_index],
1432            midpoint,
1433            approach_transition_pos,
1434            side: docking_side,
1435            site_id: dest_dock_positions.site_id,
1436        }
1437    }
1438
1439    pub fn airship_spawning_locations(&self) -> Vec<AirshipSpawningLocation> {
1440        // collect all spawning locations from all routes
1441        self.routes
1442            .iter()
1443            .flat_map(|route| route.spawning_locations.iter())
1444            .cloned()
1445            .collect()
1446    }
1447
1448    /// Get the position a route leg originates from.
1449    pub fn route_leg_departure_location(&self, route_index: usize, leg_index: usize) -> Vec2<f32> {
1450        if route_index >= self.routes.len() || leg_index >= self.routes[route_index].legs.len() {
1451            error!("Invalid index: rt {}, leg {}", route_index, leg_index);
1452            return Vec2::zero();
1453        }
1454
1455        let prev_leg = if leg_index == 0 {
1456            &self.routes[route_index].legs[self.routes[route_index].legs.len() - 1]
1457        } else {
1458            &self.routes[route_index].legs[leg_index - 1]
1459        };
1460
1461        self.airship_docks[prev_leg.dest_index]
1462            .docking_position(prev_leg.platform)
1463            .xy()
1464    }
1465
1466    /// Get the position and direction for the airship to dock at the given
1467    /// docking position. If use_starboard_boarding is None, the side for
1468    /// boarding is randomly chosen. The center of the airship position with
1469    /// respect to the docking position is an asymmetrical offset depending on
1470    /// which side of the airship will be used for boarding and where the
1471    /// captain is located on the airship. The returned position is the
1472    /// position where the captain will be when the airship is docked
1473    /// (because the captain NPC is the one that is positioned in the agent
1474    /// or rtsim code).
1475    pub fn airship_vec_for_docking_pos(
1476        docking_pos: Vec3<f32>,
1477        airship_dock_center: Vec2<f32>,
1478        docking_side: Option<AirshipDockingSide>,
1479    ) -> (Vec3<f32>, Dir) {
1480        // choose a random side for docking if not specified
1481        let dock_side = docking_side.unwrap_or_else(|| {
1482            if rand::rng().random::<bool>() {
1483                AirshipDockingSide::Starboard
1484            } else {
1485                AirshipDockingSide::Port
1486            }
1487        });
1488        // Get the vector from the dock alignment position on the airship to the
1489        // captain's position and the rotation angle for the ship to dock on the
1490        // specified side. The dock alignment position is where the airship
1491        // should touch or come closest to the dock. The side_rotation is the
1492        // angle the ship needs to rotate from to be perpendicular to the vector
1493        // from the dock center to the docking position. For example, if the docking
1494        // position is directly north (0 degrees, or aligned with the unit_y
1495        // vector), the ship needs to rotate 90 degrees CCW to dock on the port
1496        // side or 270 degrees CCW to dock on the starboard side.
1497        let (dock_align_to_captain, side_rotation) = if dock_side == AirshipDockingSide::Starboard {
1498            (
1499                Airships::DOCK_ALIGN_POS_STARBOARD,
1500                3.0 * std::f32::consts::FRAC_PI_2,
1501            )
1502        } else {
1503            (Airships::DOCK_ALIGN_POS_PORT, std::f32::consts::FRAC_PI_2)
1504        };
1505        // get the vector from the dock center to the docking platform point where the
1506        // airship should touch or come closest to.
1507        let dock_pos_offset = (docking_pos - airship_dock_center).xy();
1508        // The airship direction when docked is the dock_pos_offset rotated by the
1509        // side_rotation angle.
1510        let airship_dir =
1511            Dir::from_unnormalized(dock_pos_offset.rotated_z(side_rotation).with_z(0.0))
1512                .unwrap_or_default();
1513        // The dock_align_to_captain vector is rotated by the angle between unit_y and
1514        // the airship direction.
1515        let ship_dock_rotation =
1516            Airships::angle_between_vectors_ccw(Vec2::unit_y(), airship_dir.vec().xy());
1517        let captain_offset = dock_align_to_captain
1518            .rotated_z(ship_dock_rotation)
1519            .with_z(Airships::AIRSHIP_TO_DOCK_Z_OFFSET);
1520
1521        // To get the location of the pilot when the ship is docked, add the
1522        // captain_offset to the docking position.
1523        (docking_pos + captain_offset, airship_dir)
1524    }
1525
1526    /// Returns the angle from vec v1 to vec v2 in the CCW direction.
1527    pub fn angle_between_vectors_ccw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
1528        let dot_product = v1.dot(v2);
1529        let det = v1.x * v2.y - v1.y * v2.x; // determinant
1530        let angle = det.atan2(dot_product); // atan2(det, dot_product) gives the CCW angle
1531        if angle < 0.0 {
1532            angle + std::f32::consts::TAU
1533        } else {
1534            angle
1535        }
1536    }
1537
1538    /// Returns the angle from vec v1 to vec v2 in the CW direction.
1539    pub fn angle_between_vectors_cw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
1540        let ccw_angle = Airships::angle_between_vectors_ccw(v1, v2);
1541        std::f32::consts::TAU - ccw_angle
1542    }
1543}
1544
1545fn time_is_in_cruise_phase(time: f32, cruise_segments: &[(f32, f32)]) -> bool {
1546    for seg in cruise_segments {
1547        if time >= seg.0 && time < seg.1 {
1548            return true;
1549        }
1550        if seg.1 > time {
1551            // segments are in order, so if this segment ends after the time,
1552            // no need to check further segments
1553            break;
1554        }
1555    }
1556    false
1557}
1558
1559#[cfg(debug_assertions)]
1560macro_rules! debug_airship_eulerization {
1561    ($($arg:tt)*) => {
1562        tracing::trace!($($arg)*);
1563    };
1564}
1565
1566#[cfg(not(debug_assertions))]
1567macro_rules! debug_airship_eulerization {
1568    ($($arg:tt)*) => {};
1569}
1570
1571/// A map of node index to DockNode, where DockNode contains a list of
1572/// nodes that the node is connected to.
1573type DockNodeGraph = DHashMap<usize, DockNode>;
1574
1575/// Extension functions for Triangulation (from the triangulate crate).
1576trait TriangulationExt {
1577    fn all_edges(&self) -> DHashSet<(usize, usize)>;
1578    fn is_hull_node(&self, index: usize) -> bool;
1579    fn node_connections(&self) -> DockNodeGraph;
1580    fn eulerized_route_segments(
1581        &self,
1582        all_dock_points: &[Point],
1583        iterations: usize,
1584        max_route_leg_length: f64,
1585        seed: u32,
1586    ) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)>;
1587}
1588
1589/// Find the first node in the graph where the DockNode has an odd number of
1590/// connections to other nodes.
1591fn first_odd_node(
1592    search_order: &[usize],
1593    start: usize,
1594    nodes: &DockNodeGraph,
1595) -> Option<(usize, usize)> {
1596    search_order
1597        .iter()
1598        .enumerate()
1599        .skip(start)
1600        .find_map(|(index, &node_index)| {
1601            if let Some(dock_node) = nodes.get(&node_index) {
1602                if dock_node.connected.len() % 2 == 1 {
1603                    Some((index, node_index))
1604                } else {
1605                    None
1606                }
1607            } else {
1608                None
1609            }
1610        })
1611}
1612
1613/// Removes an edge between two nodes in the tesselation graph.
1614fn remove_edge(edge: (usize, usize), nodes: &mut DockNodeGraph) {
1615    if let Some(dock_node) = nodes.get_mut(&edge.0) {
1616        // Remove the edge from node_id1 to node_id2.
1617        // The edge may be present more than once, just remove one instance.
1618        if let Some(index) = dock_node
1619            .connected
1620            .iter()
1621            .position(|&node_id| node_id == edge.1)
1622        {
1623            dock_node.connected.remove(index);
1624        }
1625    }
1626    if let Some(dock_node) = nodes.get_mut(&edge.1) {
1627        // Remove the edge from node_id2 to node_id1.
1628        // The edge may be present more than once, just remove one instance.
1629        if let Some(index) = dock_node
1630            .connected
1631            .iter()
1632            .position(|&node_id| node_id == edge.0)
1633        {
1634            dock_node.connected.remove(index);
1635        }
1636    }
1637}
1638
1639/// Adds an edge between two nodes in the tesselation graph.
1640fn add_edge(edge: (usize, usize), nodes: &mut DockNodeGraph) {
1641    if let Some(dock_node) = nodes.get_mut(&edge.0) {
1642        dock_node.connected.push(edge.1);
1643    }
1644    if let Some(dock_node) = nodes.get_mut(&edge.1) {
1645        dock_node.connected.push(edge.0);
1646    }
1647}
1648
1649/// Implementation of extension functions for the Triangulation struct.
1650impl TriangulationExt for Triangulation {
1651    /// Get the set of all edges in the triangulation.
1652    fn all_edges(&self) -> DHashSet<(usize, usize)> {
1653        let mut edges = DHashSet::default();
1654        for t in self.triangles.chunks(3) {
1655            let a = t[0];
1656            let b = t[1];
1657            let c = t[2];
1658            // The edges hashset must have edges specified in increasing order to avoid
1659            // duplicates.
1660            edges.insert(if a < b { (a, b) } else { (b, a) });
1661            edges.insert(if b < c { (b, c) } else { (c, b) });
1662            edges.insert(if a < c { (a, c) } else { (c, a) });
1663        }
1664        edges
1665    }
1666
1667    /// For all triangles in the tessellation, create a map of nodes to their
1668    /// connected nodes.
1669    fn node_connections(&self) -> DockNodeGraph {
1670        let mut connections = DHashMap::default();
1671
1672        self.triangles.chunks(3).for_each(|t| {
1673            for &node in t {
1674                let dock_node = connections.entry(node).or_insert_with(|| DockNode {
1675                    node_id: node,
1676                    on_hull: self.is_hull_node(node),
1677                    connected: Vec::default(),
1678                });
1679                for &connected_node in t {
1680                    if connected_node != node && !dock_node.connected.contains(&connected_node) {
1681                        dock_node.connected.push(connected_node);
1682                    }
1683                }
1684            }
1685        });
1686        for (_, dock_node) in connections.iter_mut() {
1687            dock_node.connected = dock_node.connected.to_vec();
1688        }
1689        connections
1690    }
1691
1692    /// True if the node is on the outer hull of the triangulation.
1693    fn is_hull_node(&self, index: usize) -> bool { self.hull.contains(&index) }
1694
1695    /// Calculates the best way to modify the triangulation so that
1696    /// all nodes have an even number of connections (all nodes have
1697    /// an even 'degree'). The steps are:
1698    ///
1699    /// 1. Remove very long edges (not important for eurelization, but this is a
1700    ///    goal of the airship routes design.
1701    /// 2. Remove the shortest edges from all nodes that have more than 8
1702    ///    connections to other nodes. This is because the airship docking sites
1703    ///    have at most 4 docking positions, and for deconfliction purposes, no
1704    ///    two "routes" can use the same docking position.
1705    /// 3. Add edges to the triangulation so that all nodes have an even number
1706    ///    of connections to other nodes. There are many combinations of added
1707    ///    edges that can make all nodes have an even number of connections. The
1708    ///    goal is to find a graph with the maximum number of 'routes'
1709    ///    (sub-graphs of connected nodes that form a closed loop), where the
1710    ///    routes are all the same length. Since this is a graph, the algorithm
1711    ///    is sensitive to the starting point. Several iterations are tried with
1712    ///    different starting points (node indices), and the best result is
1713    ///    returned.
1714    ///
1715    /// Returns a tuple with the following elements:
1716    ///  - best_route_segments (up to 4 routes, each route is a vector of node
1717    ///    indices)
1718    ///  - best_circuit (the full eulerian circuit)
1719    ///  - max_seg_len (the length of the longest route segment)
1720    ///  - min_spread (the standard deviation of the route segment lengths)
1721    ///  - best_iteration (for debugging, the iteration that produced the best
1722    ///    result)
1723    fn eulerized_route_segments(
1724        &self,
1725        all_dock_points: &[Point],
1726        iterations: usize,
1727        max_route_leg_length: f64,
1728        seed: u32,
1729    ) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)> {
1730        let mut edges_to_remove = DHashSet::default();
1731
1732        // There can be at most four incoming and four outgoing edges per node because
1733        // there are only four docking positions per docking site and for deconfliction
1734        // purposes, no two "routes" can use the same docking position. This means that
1735        // the maximum number of edges per node is 8. Remove the shortest edges from
1736        // nodes with more than 8 edges.
1737
1738        // The tessellation algorithm produces convex hull, and there can be edges
1739        // connecting outside nodes where the distance between the points is a
1740        // significant fraction of the hull diameter. We want to keep airship
1741        // route legs as short as possible, while not removing interior edges
1742        // that may already be fairly long due to the configuration of the
1743        // docking sites relative to the entire map. For the standard world map,
1744        // with 2^10 chunks (1024x1024), the hull diameter is about 1000 chunks.
1745        // Experimentally, the standard world map can have interior edges that are
1746        // around 800 chunks long. A world map with 2^12 chunks (4096x4096) can
1747        // have hull edges that are around 2000 chunks long, but interior edges
1748        // still have a max of around 800 chunks. For the larger world maps,
1749        // removing edges that are longer than 1000 chunks is a good heuristic.
1750
1751        // First, use these heuristics to remove excess edges from the node graph.
1752        // 1. remove edges that are longer than 1000 blocks.
1753        // 2. remove the shortest edges from nodes with more than 8 edges
1754
1755        let max_distance_squared = max_route_leg_length.powi(2);
1756
1757        let all_edges = self.all_edges();
1758        for edge in all_edges.iter() {
1759            let pt1 = &all_dock_points[edge.0];
1760            let pt2 = &all_dock_points[edge.1];
1761            let v1 = Vec2 { x: pt1.x, y: pt1.y };
1762            let v2 = Vec2 { x: pt2.x, y: pt2.y };
1763            // Remove the edge if the distance between the points is greater than
1764            // max_leg_length
1765            if v1.distance_squared(v2) > max_distance_squared {
1766                edges_to_remove.insert(*edge);
1767            }
1768        }
1769
1770        #[cfg(debug_assertions)]
1771        let long_edges = edges_to_remove.len();
1772
1773        debug_airship_eulerization!(
1774            "Found {} long edges to remove out of {} total edges",
1775            edges_to_remove.len(),
1776            all_edges.len()
1777        );
1778
1779        let node_connections = self.node_connections();
1780        node_connections.iter().for_each(|(&node_id, node)| {
1781            if node.connected.len() > 8 {
1782                let excess_edges_count = node.connected.len() - 8;
1783                // Find the shortest edge and remove it
1784                let mut connected_node_info = node
1785                    .connected
1786                    .iter()
1787                    .map(|&connected_node_id| {
1788                        let pt1 = &all_dock_points[node_id];
1789                        let pt2 = &all_dock_points[connected_node_id];
1790                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1791                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1792                        (connected_node_id, v1.distance_squared(v2) as i64)
1793                    })
1794                    .collect::<Vec<_>>();
1795                connected_node_info.sort_by(|a, b| a.1.cmp(&b.1));
1796                let mut excess_edges_remaining = excess_edges_count;
1797                let mut remove_index = 0;
1798                while excess_edges_remaining > 0 && remove_index < connected_node_info.len() {
1799                    let (connected_node_id, _) = connected_node_info[remove_index];
1800                    let edge = if node_id < connected_node_id {
1801                        (node_id, connected_node_id)
1802                    } else {
1803                        (connected_node_id, node_id)
1804                    };
1805                    if !edges_to_remove.contains(&edge) {
1806                        edges_to_remove.insert(edge);
1807                        excess_edges_remaining -= 1;
1808                    }
1809                    remove_index += 1;
1810                }
1811            }
1812        });
1813
1814        let mut mutable_node_connections = node_connections.clone();
1815
1816        debug_airship_eulerization!(
1817            "Removing {} long edges and {} excess edges for a total of {} removed edges out of a \
1818             total of {} edges",
1819            long_edges,
1820            edges_to_remove.len() - long_edges,
1821            edges_to_remove.len(),
1822            all_edges.len(),
1823        );
1824
1825        for edge in edges_to_remove {
1826            remove_edge(edge, &mut mutable_node_connections);
1827        }
1828
1829        #[cfg(debug_assertions)]
1830        {
1831            // count the number of nodes with an odd connected count
1832            let odd_connected_count0 = mutable_node_connections
1833                .iter()
1834                .filter(|(_, node)| node.connected.len() % 2 == 1)
1835                .count();
1836            let total_connections1 = mutable_node_connections
1837                .iter()
1838                .map(|(_, node)| node.connected.len())
1839                .sum::<usize>();
1840            debug_airship_eulerization!(
1841                "After Removing, odd connected count: {} in {} nodes, total connections: {}",
1842                odd_connected_count0,
1843                mutable_node_connections.len(),
1844                total_connections1
1845            );
1846        }
1847
1848        // Now eurlerize the node graph by adding edges to connect nodes with an odd
1849        // number of connections. Eurlerization means that every node will have
1850        // an even number of degrees (edges), which is a requirement for
1851        // creating a Eulerian Circuit.
1852
1853        // Get the keys (node ids, which is the same as the node's index in the
1854        // all_dock_points vector) of nodes with an odd number of edges.
1855        let mut odd_keys: Vec<usize> = mutable_node_connections
1856            .iter()
1857            .filter(|(_, node)| node.connected.len() % 2 == 1)
1858            .map(|(node_id, _)| *node_id)
1859            .collect();
1860
1861        let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
1862
1863        // There will always be an even number of odd nodes in a connected graph (one
1864        // where all nodes are reachable from any other node). The goal is to
1865        // pair the odd nodes, adding edges between each pair such that the
1866        // added edges are as short as possible. After adding edges, the graph
1867        // will have an even number of edges for each node.
1868
1869        // The starting node index for finding pairs is arbitrary, and starting from
1870        // different nodes will yield different Eulerian circuits.
1871
1872        // Do a number of iterations and find the best results. The criteria is
1873        // 1. The number of route groups (the outer Vec in best_route_segments) This
1874        //    will be a maximum of 4 because there are at most 4 docking positions per
1875        //    docking site. More is better.
1876        // 2. The 'spread' of the lengths of the inner Vecs in best_route_segments. The
1877        //    calculated spread is the standard deviation of the lengths. Smaller is
1878        //    better (more uniform lengths of the route groups.)
1879        let mut best_circuit = Vec::new();
1880        let mut best_route_segments = Vec::new();
1881        let mut best_max_seg_len = 0;
1882        let mut best_min_spread = f32::MAX;
1883        let mut best_iteration = 0;
1884
1885        for i in 0..iterations {
1886            // Deterministically randomize the node order to search for the best route
1887            // segments.
1888            let mut eulerized_node_connections = mutable_node_connections.clone();
1889
1890            let mut odd_connected_count = odd_keys.len();
1891            assert!(
1892                odd_connected_count.is_multiple_of(2),
1893                "Odd connected count should be even, got {}",
1894                odd_connected_count
1895            );
1896            assert!(
1897                odd_keys.len()
1898                    == eulerized_node_connections
1899                        .iter()
1900                        .filter(|(_, node)| node.connected.len() % 2 == 1)
1901                        .count()
1902            );
1903
1904            // It's possible that the graphs starts with no odd nodes after removing edges
1905            // above.
1906            if odd_connected_count > 0 {
1907                odd_keys.shuffle(&mut rng);
1908
1909                // The edges to be added. An edge is a tuple of two node ids/indices.
1910                let mut edges_to_add = DHashSet::default();
1911                // Each odd node will be paired with only one other odd node.
1912                // Keep track of which nodes have been paired already.
1913                let mut paired_odd_nodes = DHashSet::default();
1914
1915                for node_key in odd_keys.iter() {
1916                    // Skip nodes that are already paired.
1917                    if paired_odd_nodes.contains(node_key) {
1918                        continue;
1919                    }
1920                    if let Some(node) = mutable_node_connections.get(node_key) {
1921                        // find the closest node other than nodes that are already connected to
1922                        // this one.
1923                        let mut closest_node_id = None;
1924                        let mut closest_distance = f64::MAX;
1925                        for candidate_key in odd_keys.iter() {
1926                            // Skip nodes that are already paired.
1927                            if paired_odd_nodes.contains(candidate_key) {
1928                                continue;
1929                            }
1930                            if let Some(candidate_node) =
1931                                mutable_node_connections.get(candidate_key)
1932                            {
1933                                // Skip the node itself and nodes that are already connected to this
1934                                // node.
1935                                if *candidate_key != *node_key
1936                                    && !node.connected.contains(candidate_key)
1937                                    && !candidate_node.connected.contains(node_key)
1938                                {
1939                                    // make sure the edge is specified in increasing node index
1940                                    // order to
1941                                    // avoid duplicates.
1942                                    let edge_to_add = if *node_key < *candidate_key {
1943                                        (*node_key, *candidate_key)
1944                                    } else {
1945                                        (*candidate_key, *node_key)
1946                                    };
1947                                    // Skip the edge if it is already in the edges_to_add set.
1948                                    if !edges_to_add.contains(&edge_to_add) {
1949                                        let pt1 = &all_dock_points[*node_key];
1950                                        let pt2 = &all_dock_points[*candidate_key];
1951                                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1952                                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1953                                        let distance = v1.distance_squared(v2);
1954                                        if distance < closest_distance {
1955                                            closest_distance = distance;
1956                                            closest_node_id = Some(*candidate_key);
1957                                        }
1958                                    }
1959                                }
1960                            }
1961                        }
1962                        // It's possible that the only odd nodes remaining are already connected to
1963                        // this node, but we still need to pair them. In
1964                        // this case, the connections become bidirectional,
1965                        // but that's okay for Eulerization and airships will still only follow each
1966                        // other in one direction.
1967                        if closest_node_id.is_none() {
1968                            // If no suitable node was found, repeat the search but allow
1969                            // connecting to nodes that are already connected to this one.
1970                            for candidate_key in odd_keys.iter() {
1971                                // Skip nodes that are already paired.
1972                                if paired_odd_nodes.contains(candidate_key) {
1973                                    continue;
1974                                }
1975                                // Skip the node itself
1976                                if *candidate_key != *node_key {
1977                                    // make sure the edge is specified in increasing node index
1978                                    // order to
1979                                    // avoid duplicates.
1980                                    let edge_to_add = if *node_key < *candidate_key {
1981                                        (*node_key, *candidate_key)
1982                                    } else {
1983                                        (*candidate_key, *node_key)
1984                                    };
1985                                    // Skip the edge if it is already in the edges_to_add set.
1986                                    if !edges_to_add.contains(&edge_to_add) {
1987                                        let pt1 = &all_dock_points[*node_key];
1988                                        let pt2 = &all_dock_points[*candidate_key];
1989                                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1990                                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1991                                        let distance = v1.distance_squared(v2);
1992                                        if distance < closest_distance {
1993                                            closest_distance = distance;
1994                                            closest_node_id = Some(*candidate_key);
1995                                        }
1996                                    }
1997                                }
1998                            }
1999                        }
2000                        // If a closest node was found that is not already paired, add the edge.
2001                        // Note that this should not fail since we are guaranteed to have
2002                        // an even number of odd nodes.
2003                        if let Some(close_node_id) = closest_node_id {
2004                            // add the edge between node_id and closest_node_id
2005                            let edge_to_add = if *node_key < close_node_id {
2006                                (*node_key, close_node_id)
2007                            } else {
2008                                (close_node_id, *node_key)
2009                            };
2010                            edges_to_add.insert(edge_to_add);
2011                            paired_odd_nodes.insert(*node_key);
2012                            paired_odd_nodes.insert(close_node_id);
2013                        } else {
2014                            error!("Cannot pair all odd nodes, this should not happen.");
2015                        }
2016                    }
2017                    if edges_to_add.len() == odd_connected_count / 2 {
2018                        // If we have paired all odd nodes, break out of the loop.
2019                        // The break is necessary because the outer loop iterates over
2020                        // all odd keys but we only need make 1/2 that many pairs of nodes.
2021                        break;
2022                    }
2023                }
2024                for edge in edges_to_add {
2025                    add_edge(edge, &mut eulerized_node_connections);
2026                }
2027                // count the number of nodes with an odd connected count
2028                odd_connected_count = eulerized_node_connections
2029                    .iter()
2030                    .filter(|(_, node)| node.connected.len() % 2 == 1)
2031                    .count();
2032
2033                #[cfg(debug_assertions)]
2034                {
2035                    let total_connections = eulerized_node_connections
2036                        .iter()
2037                        .map(|(_, node)| node.connected.len())
2038                        .sum::<usize>();
2039                    debug_airship_eulerization!(
2040                        "Outer Iteration: {}, After Adding, odd connected count: {} in {} nodes, \
2041                         total connections: {}",
2042                        i,
2043                        odd_connected_count,
2044                        eulerized_node_connections.len(),
2045                        total_connections
2046                    );
2047                }
2048            }
2049
2050            // If all nodes have an even number of edges, proceed with finding the best
2051            // Eulerian circuit for the given node configuration.
2052            if odd_connected_count == 0 {
2053                // Find the best Eulerian circuit for the current node connections
2054                if let Some((route_segments, circuit, max_seg_len, min_spread, _)) =
2055                    find_best_eulerian_circuit(&eulerized_node_connections)
2056                {
2057                    #[cfg(debug_assertions)]
2058                    {
2059                        debug_airship_eulerization!("Outer Iteration: {}", i);
2060                        debug_airship_eulerization!("Max segment length: {}", max_seg_len);
2061                        debug_airship_eulerization!("Min spread: {}", min_spread);
2062                        debug_airship_eulerization!("Segments count:");
2063                        route_segments.iter().enumerate().for_each(|segment| {
2064                            debug_airship_eulerization!("  {} : {}", segment.0, segment.1.len());
2065                        });
2066                    }
2067                    // A Eulerian circuit was found, apply the goal criteria to find the best
2068                    // circuit.
2069                    if max_seg_len > best_max_seg_len
2070                        || (max_seg_len == best_max_seg_len && min_spread < best_min_spread)
2071                    {
2072                        best_circuit = circuit;
2073                        best_route_segments = route_segments;
2074                        best_max_seg_len = max_seg_len;
2075                        best_min_spread = min_spread;
2076                        best_iteration = i;
2077                    }
2078                }
2079            } else {
2080                debug_airship_eulerization!(
2081                    "Error, this should not happen: iteration {}, odd connected count: {} of {} \
2082                     nodes, total connections: {}, SKIPPING iteration",
2083                    i,
2084                    odd_connected_count,
2085                    eulerized_node_connections.len(),
2086                    eulerized_node_connections
2087                        .iter()
2088                        .map(|(_, node)| node.connected.len())
2089                        .sum::<usize>()
2090                );
2091                error!(
2092                    "Eulerian circuit not found on iteration {}. Odd connected count is not zero, \
2093                     this should not happen",
2094                    i
2095                );
2096            }
2097        }
2098        #[cfg(debug_assertions)]
2099        {
2100            debug_airship_eulerization!("Max segment length: {}", best_max_seg_len);
2101            debug_airship_eulerization!("Min spread: {}", best_min_spread);
2102            debug_airship_eulerization!("Iteration: {}", best_iteration);
2103            debug_airship_eulerization!("Segments count:");
2104            best_route_segments.iter().enumerate().for_each(|segment| {
2105                debug_airship_eulerization!("  {} : {}", segment.0, segment.1.len());
2106            });
2107        }
2108
2109        if best_route_segments.is_empty() {
2110            return None;
2111        }
2112        Some((
2113            best_route_segments,
2114            best_circuit,
2115            best_max_seg_len,
2116            best_min_spread,
2117            best_iteration,
2118        ))
2119    }
2120}
2121
2122/// Find the best Eulerian circuit for the given graph of dock nodes.
2123/// Try each node as the starting point for a circuit.
2124/// The best circuit is the one with the longest routes (sub-segments
2125/// of the circuit), and where the route lengths are equal as possible.
2126fn find_best_eulerian_circuit(
2127    graph: &DockNodeGraph,
2128) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)> {
2129    let mut best_circuit = Vec::new();
2130    let mut best_route_segments = Vec::new();
2131    let mut best_max_seg_len = 0;
2132    let mut best_min_spread = f32::MAX;
2133    let mut best_iteration = 0;
2134
2135    let graph_keys = graph.keys().copied().collect::<Vec<_>>();
2136
2137    // Repeat for each node as the starting point.
2138    for (i, &start_vertex) in graph_keys.iter().enumerate() {
2139        let mut graph = graph.clone();
2140        let mut circuit = Vec::new();
2141        let mut stack = Vec::new();
2142        let mut circuit_nodes = DHashSet::default();
2143
2144        let mut current_vertex = start_vertex;
2145
2146        // The algorithm for finding a Eulerian Circuit (Hierholzer's algorithm).
2147        while !stack.is_empty() || !graph[&current_vertex].connected.is_empty() {
2148            if graph[&current_vertex].connected.is_empty() {
2149                circuit.push(current_vertex);
2150                circuit_nodes.insert(current_vertex);
2151                current_vertex = stack.pop()?;
2152            } else {
2153                stack.push(current_vertex);
2154                if let Some(&next_vertex) = graph
2155                    .get(&current_vertex)?
2156                    .connected
2157                    .iter()
2158                    .find(|&vertex| !circuit_nodes.contains(vertex))
2159                    .or(graph.get(&current_vertex)?.connected.first())
2160                {
2161                    remove_edge((current_vertex, next_vertex), &mut graph);
2162                    current_vertex = next_vertex;
2163                } else {
2164                    return None;
2165                }
2166            }
2167        }
2168        circuit.push(current_vertex);
2169        circuit.reverse();
2170
2171        if let Some((route_segments, max_seg_len, min_spread)) =
2172            best_eulerian_circuit_segments(&graph, &circuit)
2173            && (max_seg_len > best_max_seg_len
2174                || (max_seg_len == best_max_seg_len && min_spread < best_min_spread))
2175        {
2176            best_circuit = circuit.clone();
2177            best_route_segments = route_segments;
2178            best_max_seg_len = max_seg_len;
2179            best_min_spread = min_spread;
2180            best_iteration = i;
2181        }
2182    }
2183    if best_route_segments.is_empty() {
2184        return None;
2185    }
2186    Some((
2187        best_route_segments,
2188        best_circuit,
2189        best_max_seg_len,
2190        best_min_spread,
2191        best_iteration,
2192    ))
2193}
2194
2195/// Get the optimal grouping of Eulerian Circuit nodes and edges such that a
2196/// maximum number of sub-circuits are created, and the length of each
2197/// sub-circuit is as similar as possible.
2198///
2199/// The Airship dock nodes are connected in a Eulerian Circuit, where each edge
2200/// of the tessellation is traversed exactly once. The circuit is a closed loop,
2201/// so the first and last node are the same. The single circuit can be broken
2202/// into multiple segments, each being also a closed loop. This is desirable for
2203/// airship routes, to limit the number of airships associated with each "route"
2204/// where a route is a closed circuit of docking sites. Since each edge is flown
2205/// in only one direction, the maximum number of possible closed loop segments
2206/// is equal to the maximum number of edges connected to any node, divided by 2.
2207fn best_eulerian_circuit_segments(
2208    graph: &DockNodeGraph,
2209    circuit: &[usize],
2210) -> Option<(Vec<Vec<usize>>, usize, f32)> {
2211    // get the node_connections keys, which are node ids.
2212    // Sort the nodes (node ids) by the number of connections to other nodes.
2213    let sorted_node_ids: Vec<usize> = graph
2214        .keys()
2215        .copied()
2216        .sorted_by_key(|&node_id| graph[&node_id].connected.len())
2217        .rev()
2218        .collect();
2219
2220    let mut max_segments_count = 0;
2221    let mut min_segments_len_spread = f32::MAX;
2222    let mut best_segments = Vec::new();
2223
2224    // For each node_id in the sorted node ids,
2225    // break the circuit into circular segments that start and end with that
2226    // node_id. The best set of segments is the one with the most segments and
2227    // where the length of the segments differ the least.
2228    sorted_node_ids.iter().for_each(|&node_id| {
2229        let mut segments = Vec::new();
2230        let mut current_segment = Vec::new();
2231        let circuit_len = circuit.len();
2232        let mut starting_index = usize::MAX;
2233        let mut end_index = usize::MAX;
2234        let mut prev_value = usize::MAX;
2235
2236        for (index, &value) in circuit.iter().cycle().enumerate() {
2237            if value == node_id {
2238                if starting_index == usize::MAX {
2239                    starting_index = index;
2240                    if starting_index > 0 {
2241                        end_index = index + circuit_len - 1;
2242                    } else {
2243                        end_index = index + circuit_len - 2;
2244                    }
2245                }
2246                if !current_segment.is_empty() {
2247                    current_segment.push(value);
2248                    segments.push(current_segment);
2249                    current_segment = Vec::new();
2250                }
2251            }
2252            if starting_index < usize::MAX {
2253                if value != prev_value {
2254                    current_segment.push(value);
2255                }
2256                prev_value = value;
2257            }
2258
2259            // Stop cycling once we've looped back to the value before the starting index
2260            if index == end_index {
2261                break;
2262            }
2263        }
2264
2265        // Add the last segment
2266        if !current_segment.is_empty() {
2267            current_segment.push(node_id);
2268            segments.push(current_segment);
2269        }
2270
2271        let avg_segment_length = segments.iter().map(|segment| segment.len()).sum::<usize>() as f32
2272            / segments.len() as f32;
2273
2274        // We want similar segment lengths, so calculate the spread as the
2275        // standard deviation of the segment lengths.
2276        let seg_lengths_spread = segments
2277            .iter()
2278            .map(|segment| (segment.len() as f32 - avg_segment_length).powi(2))
2279            .sum::<f32>()
2280            .sqrt()
2281            / segments.len() as f32;
2282
2283        // First take the longest segment count, then if the segment count is the same
2284        // as the longest so far, take the one with the least length spread.
2285        if segments.len() > max_segments_count {
2286            max_segments_count = segments.len();
2287            min_segments_len_spread = seg_lengths_spread;
2288            best_segments = segments;
2289        } else if segments.len() == max_segments_count
2290            && seg_lengths_spread < min_segments_len_spread
2291        {
2292            min_segments_len_spread = seg_lengths_spread;
2293            best_segments = segments;
2294        }
2295    });
2296    if best_segments.is_empty() {
2297        return None;
2298    }
2299    Some((best_segments, max_segments_count, min_segments_len_spread))
2300}
2301
2302#[cfg(test)]
2303mod tests {
2304    use super::{AirshipDockPlatform, AirshipDockingSide, Airships, approx::assert_relative_eq};
2305    use vek::{Vec2, Vec3};
2306
2307    #[test]
2308    fn vec_angles_test() {
2309        let refvec = Vec3::new(0.0f32, 10.0, 0.0);
2310
2311        let vec1 = Vec3::new(0.0f32, 10.0, 0.0);
2312        let vec2 = Vec3::new(10.0f32, 0.0, 0.0);
2313        let vec3 = Vec3::new(0.0f32, -10.0, 0.0);
2314        let vec4 = Vec3::new(-10.0f32, 0.0, 0.0);
2315
2316        let a1r = vec1.angle_between(refvec);
2317        let a1r3 = Airships::angle_between_vectors_ccw(vec1.xy(), refvec.xy());
2318        assert!(a1r == 0.0f32);
2319        assert!(a1r3 == 0.0f32);
2320
2321        let a2r = vec2.angle_between(refvec);
2322        let a2r3 = Airships::angle_between_vectors_ccw(vec2.xy(), refvec.xy());
2323        assert_relative_eq!(a2r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
2324        assert_relative_eq!(a2r3, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
2325
2326        let a3r: f32 = vec3.angle_between(refvec);
2327        let a3r3 = Airships::angle_between_vectors_ccw(vec3.xy(), refvec.xy());
2328        assert_relative_eq!(a3r, std::f32::consts::PI, epsilon = 0.00001);
2329        assert_relative_eq!(a3r3, std::f32::consts::PI, epsilon = 0.00001);
2330
2331        let a4r = vec4.angle_between(refvec);
2332        let a4r3 = Airships::angle_between_vectors_ccw(vec4.xy(), refvec.xy());
2333        assert_relative_eq!(a4r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
2334        assert_relative_eq!(a4r3, std::f32::consts::FRAC_PI_2 * 3.0, epsilon = 0.00001);
2335    }
2336
2337    #[test]
2338    fn airship_angles_test() {
2339        let refvec = Vec2::new(0.0f32, 37.0);
2340        let ovec = Vec2::new(-4.0f32, -14.0);
2341        let oveccw0 = Vec2::new(-4, -14);
2342        let oveccw90 = Vec2::new(-14, 4);
2343        let oveccw180 = Vec2::new(4, 14);
2344        let oveccw270 = Vec2::new(14, -4);
2345        let ovecccw0 = Vec2::new(-4, -14);
2346        let ovecccw90 = Vec2::new(14, -4);
2347        let ovecccw180 = Vec2::new(4, 14);
2348        let ovecccw270 = Vec2::new(-14, 4);
2349
2350        let vec1 = Vec2::new(0.0f32, 37.0);
2351        let vec2 = Vec2::new(37.0f32, 0.0);
2352        let vec3 = Vec2::new(0.0f32, -37.0);
2353        let vec4 = Vec2::new(-37.0f32, 0.0);
2354
2355        assert!(
2356            ovec.rotated_z(Airships::angle_between_vectors_cw(vec1, refvec))
2357                .map(|x| x.round() as i32)
2358                == oveccw0
2359        );
2360        assert!(
2361            ovec.rotated_z(Airships::angle_between_vectors_cw(vec2, refvec))
2362                .map(|x| x.round() as i32)
2363                == oveccw90
2364        );
2365        assert!(
2366            ovec.rotated_z(Airships::angle_between_vectors_cw(vec3, refvec))
2367                .map(|x| x.round() as i32)
2368                == oveccw180
2369        );
2370        assert!(
2371            ovec.rotated_z(Airships::angle_between_vectors_cw(vec4, refvec))
2372                .map(|x| x.round() as i32)
2373                == oveccw270
2374        );
2375
2376        assert!(
2377            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec1, refvec))
2378                .map(|x| x.round() as i32)
2379                == ovecccw0
2380        );
2381        assert!(
2382            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec2, refvec))
2383                .map(|x| x.round() as i32)
2384                == ovecccw90
2385        );
2386        assert!(
2387            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec3, refvec))
2388                .map(|x| x.round() as i32)
2389                == ovecccw180
2390        );
2391        assert!(
2392            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec4, refvec))
2393                .map(|x| x.round() as i32)
2394                == ovecccw270
2395        );
2396    }
2397
2398    #[test]
2399    fn airship_vec_test() {
2400        {
2401            let dock_pos = Vec3::new(10.0f32, 10.0, 0.0);
2402            let airship_dock_center = Vec2::new(0.0, 0.0);
2403            let mut left_tested = false;
2404            let mut right_tested = false;
2405            {
2406                for _ in 0..1000 {
2407                    let (airship_pos, airship_dir) =
2408                        Airships::airship_vec_for_docking_pos(dock_pos, airship_dock_center, None);
2409                    if airship_pos.x > 23.0 {
2410                        assert_relative_eq!(
2411                            airship_pos,
2412                            Vec3 {
2413                                x: 23.435028,
2414                                y: 22.020815,
2415                                z: -3.0
2416                            },
2417                            epsilon = 0.00001
2418                        );
2419                        assert_relative_eq!(
2420                            airship_dir.to_vec(),
2421                            Vec3 {
2422                                x: -0.70710677,
2423                                y: 0.70710677,
2424                                z: 0.0
2425                            },
2426                            epsilon = 0.00001
2427                        );
2428                        left_tested = true;
2429                    } else {
2430                        assert_relative_eq!(
2431                            airship_pos,
2432                            Vec3 {
2433                                x: 22.020815,
2434                                y: 23.435028,
2435                                z: -3.0
2436                            },
2437                            epsilon = 0.00001
2438                        );
2439                        assert_relative_eq!(
2440                            airship_dir.to_vec(),
2441                            Vec3 {
2442                                x: 0.70710677,
2443                                y: -0.70710677,
2444                                z: 0.0
2445                            },
2446                            epsilon = 0.00001
2447                        );
2448                        right_tested = true;
2449                    }
2450                    if left_tested && right_tested {
2451                        break;
2452                    }
2453                }
2454            }
2455            {
2456                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2457                    dock_pos,
2458                    airship_dock_center,
2459                    Some(AirshipDockingSide::Port),
2460                );
2461                assert_relative_eq!(
2462                    airship_pos,
2463                    Vec3 {
2464                        x: 23.435028,
2465                        y: 22.020815,
2466                        z: -3.0
2467                    },
2468                    epsilon = 0.00001
2469                );
2470                assert_relative_eq!(
2471                    airship_dir.to_vec(),
2472                    Vec3 {
2473                        x: -0.70710677,
2474                        y: 0.70710677,
2475                        z: 0.0
2476                    },
2477                    epsilon = 0.00001
2478                );
2479            }
2480            {
2481                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2482                    dock_pos,
2483                    airship_dock_center,
2484                    Some(AirshipDockingSide::Starboard),
2485                );
2486                assert_relative_eq!(
2487                    airship_pos,
2488                    Vec3 {
2489                        x: 22.020815,
2490                        y: 23.435028,
2491                        z: -3.0
2492                    },
2493                    epsilon = 0.00001
2494                );
2495                assert_relative_eq!(
2496                    airship_dir.to_vec(),
2497                    Vec3 {
2498                        x: 0.70710677,
2499                        y: -0.70710677,
2500                        z: 0.0
2501                    },
2502                    epsilon = 0.00001
2503                );
2504            }
2505        }
2506        {
2507            let dock_pos = Vec3::new(28874.0, 18561.0, 0.0);
2508            let airship_dock_center = Vec2::new(28911.0, 18561.0);
2509            {
2510                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2511                    dock_pos,
2512                    airship_dock_center,
2513                    Some(AirshipDockingSide::Port),
2514                );
2515                assert_relative_eq!(
2516                    airship_pos,
2517                    Vec3 {
2518                        x: 28856.0,
2519                        y: 18562.0,
2520                        z: -3.0
2521                    },
2522                    epsilon = 0.00001
2523                );
2524                assert_relative_eq!(
2525                    airship_dir.to_vec(),
2526                    Vec3 {
2527                        x: 4.371139e-8,
2528                        y: -1.0,
2529                        z: 0.0
2530                    },
2531                    epsilon = 0.00001
2532                );
2533            }
2534            {
2535                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
2536                    dock_pos,
2537                    airship_dock_center,
2538                    Some(AirshipDockingSide::Starboard),
2539                );
2540                assert_relative_eq!(
2541                    airship_pos,
2542                    Vec3 {
2543                        x: 28856.0,
2544                        y: 18560.0,
2545                        z: -3.0
2546                    },
2547                    epsilon = 0.00001
2548                );
2549                assert_relative_eq!(
2550                    airship_dir.to_vec(),
2551                    Vec3 {
2552                        x: -1.1924881e-8,
2553                        y: 1.0,
2554                        z: 0.0
2555                    },
2556                    epsilon = 0.00001
2557                );
2558            }
2559        }
2560    }
2561
2562    #[test]
2563    fn docking_side_for_platform_test() {
2564        // Approximately: 0, 22, 45, 67, 90, 112, 135, 157, 180, 202, 225, 247, 270,
2565        // 292, 315, 337 degrees
2566        let dirs = [
2567            Vec2::new(0.0, 100.0) - Vec2::zero(),
2568            Vec2::new(100.0, 100.0) - Vec2::zero(),
2569            Vec2::new(100.0, 0.0) - Vec2::zero(),
2570            Vec2::new(100.0, -100.0) - Vec2::zero(),
2571            Vec2::new(0.0, -100.0) - Vec2::zero(),
2572            Vec2::new(-100.0, -100.0) - Vec2::zero(),
2573            Vec2::new(-100.0, 0.0) - Vec2::zero(),
2574            Vec2::new(-100.0, 100.0) - Vec2::zero(),
2575        ];
2576        let expected = [
2577            AirshipDockingSide::Port,
2578            AirshipDockingSide::Starboard,
2579            AirshipDockingSide::Starboard,
2580            AirshipDockingSide::Starboard,
2581            AirshipDockingSide::Starboard,
2582            AirshipDockingSide::Port,
2583            AirshipDockingSide::Port,
2584            AirshipDockingSide::Port,
2585            AirshipDockingSide::Port,
2586            AirshipDockingSide::Port,
2587            AirshipDockingSide::Port,
2588            AirshipDockingSide::Starboard,
2589            AirshipDockingSide::Starboard,
2590            AirshipDockingSide::Starboard,
2591            AirshipDockingSide::Starboard,
2592            AirshipDockingSide::Port,
2593            AirshipDockingSide::Starboard,
2594            AirshipDockingSide::Port,
2595            AirshipDockingSide::Port,
2596            AirshipDockingSide::Port,
2597            AirshipDockingSide::Port,
2598            AirshipDockingSide::Starboard,
2599            AirshipDockingSide::Starboard,
2600            AirshipDockingSide::Starboard,
2601            AirshipDockingSide::Starboard,
2602            AirshipDockingSide::Starboard,
2603            AirshipDockingSide::Starboard,
2604            AirshipDockingSide::Port,
2605            AirshipDockingSide::Port,
2606            AirshipDockingSide::Port,
2607            AirshipDockingSide::Port,
2608            AirshipDockingSide::Starboard,
2609        ];
2610        for platform in [
2611            AirshipDockPlatform::NorthPlatform,
2612            AirshipDockPlatform::EastPlatform,
2613            AirshipDockPlatform::SouthPlatform,
2614            AirshipDockPlatform::WestPlatform,
2615        ]
2616        .iter()
2617        {
2618            for (i, dir) in dirs.iter().enumerate() {
2619                let side = AirshipDockingSide::from_dir_to_platform(dir, platform);
2620                assert_eq!(side, expected[*platform as usize * 8 + i]);
2621            }
2622        }
2623    }
2624}