Skip to main content

rpfm_extensions/startpos/
trade.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Trade network of a startpos (`CAMPAIGN_TRADE_MANAGER`), built from `trade_routes.ptd`.
12//!
13//! Verified against Attila's main campaign: every spline (flags, segments, points, lengths, speed tables) and every
14//! region and port route matches the game-built startpos. Not covered: the order the game lists the routes in (hash
15//! order).
16
17use getset::Getters;
18use serde_derive::{Deserialize, Serialize};
19
20use std::collections::HashSet;
21
22use rpfm_lib::files::trade_routes::{TradeRoute, TradeRouteEdge, TradeRoutes, TradeSplineSegment};
23
24use super::coords::MapScale;
25
26#[cfg(test)] #[path = "trade_test.rs"] mod trade_test;
27
28/// Gauss-Legendre nodes (on [-1, 1]) used to integrate segment lengths.
29const GAUSS_NODES: [f64; 5] = [-0.906_179_845_938_664, -0.538_469_310_105_683, 0.0, 0.538_469_310_105_683, 0.906_179_845_938_664];
30
31/// Gauss-Legendre weights matching [`GAUSS_NODES`].
32const GAUSS_WEIGHTS: [f64; 5] = [0.236_926_885_056_189, 0.478_628_670_499_366, 0.568_888_888_888_889, 0.478_628_670_499_366, 0.236_926_885_056_189];
33
34/// Subintervals each segment is split into when integrating its length.
35const LENGTH_SUBINTERVALS: usize = 64;
36
37/// Samples of the speed table of each segment.
38const SPEED_SAMPLES: usize = 100;
39
40//---------------------------------------------------------------------------//
41//                              Types
42//---------------------------------------------------------------------------//
43
44/// Trade network of a campaign, as stored in the startpos.
45#[derive(Debug, Clone, PartialEq, Default, Getters, Serialize, Deserialize)]
46#[getset(get = "pub")]
47pub struct TradeNetwork {
48
49    /// Region keys. Routes refer to regions by their index in this list.
50    regions: Vec<String>,
51
52    /// Splines, in file order (`CAMPAIGN_TRADE_MANAGER/SPLINES`).
53    splines: Vec<NetworkSpline>,
54
55    /// One route per region pair (`REGION_ROUTES`).
56    region_routes: Vec<NetworkRoute>,
57
58    /// One route per port pair (`PORT_ROUTES`).
59    port_routes: Vec<NetworkRoute>,
60}
61
62/// A trade spline in world coordinates.
63#[derive(Debug, Clone, PartialEq, Default, Getters, Serialize, Deserialize)]
64#[getset(get = "pub")]
65pub struct NetworkSpline {
66
67    /// Flags copied from the map file.
68    flags: [bool; 4],
69
70    /// Bezier control points of each segment: start, first control, second control, end.
71    segments: Vec<[(f32, f32); 4]>,
72
73    /// Speed table of each segment: the running sum of the curve's speed at `t = k / 100`, for k from 0 to 99.
74    speed_tables: Vec<Vec<f32>>,
75
76    /// Length of each segment.
77    lengths: Vec<f32>,
78
79    /// Total length.
80    total_length: f32,
81}
82
83/// A route between two regions or ports.
84#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, Serialize, Deserialize)]
85#[getset(get = "pub")]
86pub struct NetworkRoute {
87
88    /// Index of the origin in the map file's region table.
89    from: u32,
90
91    /// Index of the destination in the map file's region table.
92    to: u32,
93
94    /// Spline edges to follow.
95    edges: Vec<TradeRouteEdge>,
96}
97
98//---------------------------------------------------------------------------//
99//                              Implementations
100//---------------------------------------------------------------------------//
101
102impl TradeNetwork {
103
104    /// Builds the trade network of a startpos from the campaign map's trade routes file.
105    ///
106    /// # Arguments
107    ///
108    /// * `trade_routes` - Decoded `trade_routes.ptd` of the campaign map.
109    /// * `scale` - Scale between the campaign map's world and raw units.
110    ///
111    /// # Returns
112    ///
113    /// The splines with their lengths, and the first route listed for each region and port pair.
114    pub fn from_trade_routes(trade_routes: &TradeRoutes, scale: &MapScale) -> Self {
115        let splines = trade_routes.splines().iter().map(|spline| {
116            let segments = spline.segments().iter().map(|segment| control_points_world(segment, scale)).collect::<Vec<_>>();
117            let lengths = segments.iter().map(bezier_length).collect::<Vec<_>>();
118            let speed_tables = segments.iter().map(speed_table).collect();
119            let total_length = lengths.iter().sum();
120            NetworkSpline {
121                flags: *spline.flags(),
122                segments,
123                speed_tables,
124                lengths,
125                total_length,
126            }
127        }).collect();
128
129        Self {
130            regions: trade_routes.regions().clone(),
131            splines,
132            region_routes: first_route_per_pair(trade_routes.region_routes()),
133            port_routes: first_route_per_pair(trade_routes.port_routes()),
134        }
135    }
136}
137
138/// Keeps the first route listed for each (from, to) pair, in order of first appearance.
139fn first_route_per_pair(routes: &[TradeRoute]) -> Vec<NetworkRoute> {
140    let mut seen = HashSet::new();
141    routes.iter()
142        .filter(|route| seen.insert((*route.from(), *route.to())))
143        .map(|route| NetworkRoute {
144            from: *route.from(),
145            to: *route.to(),
146            edges: route.edges().clone(),
147        })
148        .collect()
149}
150
151/// Returns the four Bezier control points of a segment in world coordinates: start, start + first control,
152/// end + second control, end.
153fn control_points_world(segment: &TradeSplineSegment, scale: &MapScale) -> [(f32, f32); 4] {
154    let start = scale.raw_to_world(f32::from(segment.start().0), f32::from(segment.start().1));
155    let end = scale.raw_to_world(f32::from(segment.end().0), f32::from(segment.end().1));
156    let start_offset = scale.raw_offset_to_world(segment.start_control().0, segment.start_control().1);
157    let end_offset = scale.raw_offset_to_world(segment.end_control().0, segment.end_control().1);
158    [
159        start,
160        (start.0 + start_offset.0, start.1 + start_offset.1),
161        (end.0 + end_offset.0, end.1 + end_offset.1),
162        end,
163    ]
164}
165
166/// Returns the speed of a cubic Bezier curve (the length of its derivative) at `t`.
167fn bezier_speed(points: &[(f32, f32); 4], t: f64) -> f64 {
168    let [p0, p1, p2, p3] = points.map(|(x, y)| (f64::from(x), f64::from(y)));
169    let u = 1.0 - t;
170    let dx = 3.0 * u * u * (p1.0 - p0.0) + 6.0 * u * t * (p2.0 - p1.0) + 3.0 * t * t * (p3.0 - p2.0);
171    let dy = 3.0 * u * u * (p1.1 - p0.1) + 6.0 * u * t * (p2.1 - p1.1) + 3.0 * t * t * (p3.1 - p2.1);
172    (dx * dx + dy * dy).sqrt()
173}
174
175/// Returns the speed table of a segment: the running sum of its speed at `t = k / 100`. The last entry is close to
176/// 100 times its length.
177fn speed_table(points: &[(f32, f32); 4]) -> Vec<f32> {
178    let mut sum = 0.0;
179    (0..SPEED_SAMPLES).map(|sample| {
180        sum += bezier_speed(points, sample as f64 / SPEED_SAMPLES as f64);
181        sum as f32
182    }).collect()
183}
184
185/// Returns the length of a cubic Bezier curve, integrating its speed with Gauss-Legendre quadrature.
186fn bezier_length(points: &[(f32, f32); 4]) -> f32 {
187    let speed = |t: f64| bezier_speed(points, t);
188
189    let step = 1.0 / LENGTH_SUBINTERVALS as f64;
190    let length: f64 = (0..LENGTH_SUBINTERVALS).map(|interval| {
191        let start = interval as f64 * step;
192        GAUSS_NODES.iter().zip(GAUSS_WEIGHTS.iter())
193            .map(|(node, weight)| weight * speed(start + (node + 1.0) * step / 2.0))
194            .sum::<f64>() * step / 2.0
195    }).sum();
196
197    length as f32
198}