supreme_replay_analyzer/
track_scoring.rs1use crate::models::{AnalyzeResult, Level, Position, RowData};
4use crate::plane_intersection::{
5 LevelPlaneIntersection, PlaneIntersectionInfo, get_plane_intersections_with_level_data,
6};
7
8const PLANE_RADIUS: f32 = 90.5;
9const NEAREST_START_POINT_DIST: f32 = 2.0;
10
11#[derive(Debug, Clone)]
12pub struct LevelScore {
13 pub name: String,
14 pub score: usize,
15}
16
17#[must_use]
18pub fn score_track(analyze_result: &AnalyzeResult, level_data: &[Level]) -> Option<String> {
19 let coords = analyze_result.coords.as_ref()?;
20
21 let level_plane_intersections =
22 get_plane_intersections_with_level_data(&coords.rows, level_data);
23 let level_scores = score_every_level(analyze_result, &level_plane_intersections, level_data);
24
25 if level_scores.is_empty() {
26 return None;
27 }
28
29 let top_score = level_scores[0].score;
30 if top_score == 0 {
31 return None;
32 }
33
34 let top_scoring: Vec<String> = level_scores
35 .iter()
36 .filter(|s| s.score == top_score)
37 .map(|s| s.name.clone())
38 .collect();
39
40 if top_scoring.len() == 2
42 && top_scoring.contains(&"ForestHard".to_string())
43 && top_scoring.contains(&"ForestMedium".to_string())
44 {
45 return Some("ForestMediumOrHard".to_string());
46 }
47
48 if top_scoring.len() == 1 {
49 return Some(top_scoring[0].clone());
50 }
51
52 None
53}
54
55#[must_use]
56pub fn identify_track_by_start_pos(first_row: &RowData, level_data: &[Level]) -> Option<String> {
57 let mut best_match = None;
58 let mut min_dist = f32::MAX;
59
60 for level in level_data {
61 let dist = get_nearest_player_start_location(
62 &level.name,
63 std::slice::from_ref(first_row),
64 level_data,
65 );
66 if dist < min_dist {
67 min_dist = dist;
68 best_match = Some(level.name.clone());
69 }
70 }
71
72 if min_dist <= NEAREST_START_POINT_DIST {
73 best_match
74 } else {
75 None
76 }
77}
78
79#[must_use]
80pub fn identify_track_fast(
81 start_pos: &RowData,
82 cp1_pos: &RowData,
83 finish_pos: &RowData,
84 level_data: &[Level],
85) -> Option<String> {
86 let mut best_match = None;
87 let mut min_cum_dist_sq = f32::MAX;
88
89 for level in level_data {
90 let l_start = find_closest_level_point(level, "Player_Start_Location", start_pos);
91 let l_cp1 = find_closest_level_point(level, "Check_Point_1", cp1_pos);
92 let l_finish = find_closest_level_point(level, "Finish_Point", finish_pos);
93
94 if let (Some(ls), Some(lc), Some(lf)) = (l_start, l_cp1, l_finish) {
95 let d1 = calculate_distance_sq(start_pos, ls);
96 let d2 = calculate_distance_sq(cp1_pos, lc);
97 let d3 = calculate_distance_sq(finish_pos, lf);
98
99 let cum_dist_sq = d1 + d2 + d3;
100 if cum_dist_sq < min_cum_dist_sq {
101 min_cum_dist_sq = cum_dist_sq;
102 best_match = Some(level.name.clone());
103 }
104 }
105 }
106
107 best_match
108}
109
110fn find_closest_level_point<'a>(
111 level: &'a Level,
112 name: &str,
113 target: &RowData,
114) -> Option<&'a Position> {
115 level
116 .entries
117 .iter()
118 .filter(|e| e.name == name)
119 .min_by(|a, b| {
120 let da = calculate_distance_sq(target, &a.position);
121 let db = calculate_distance_sq(target, &b.position);
122 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
123 })
124 .map(|e| &e.position)
125}
126
127fn calculate_distance_sq(p1: &RowData, p2: &Position) -> f32 {
128 let dx = p1.x - p2.x;
129 let dy = p1.y - p2.y;
130 let dz = p1.z - p2.z;
131 dx * dx + dy * dy + dz * dz
132}
133
134fn score_every_level(
135 analyze_result: &AnalyzeResult,
136 level_plane_intersections: &[LevelPlaneIntersection],
137 level_data: &[Level],
138) -> Vec<LevelScore> {
139 let coords = match &analyze_result.coords {
140 Some(c) => c,
141 None => return Vec::new(),
142 };
143
144 let mut level_scores: Vec<LevelScore> = level_plane_intersections
145 .iter()
146 .map(|lpc| {
147 let nearest_start_dist =
148 get_nearest_player_start_location(&lpc.name, &coords.rows, level_data);
149
150 let first_valid_start = get_valid_intersections(lpc, "Start_Point").first().cloned();
151 let first_valid_checkpoint1 = get_valid_intersections(lpc, "Check_Point_1")
152 .first()
153 .cloned();
154 let first_valid_finish = get_valid_intersections(lpc, "Finish_Point")
155 .first()
156 .cloned();
157
158 let all_checkpoint2: Vec<&PlaneIntersectionInfo> = lpc
159 .intersections
160 .iter()
161 .filter(|c| c.object_name == "Check_Point_2")
162 .collect();
163
164 let valid_checkpoint2_count = all_checkpoint2
165 .iter()
166 .filter(|c| c.distance <= PLANE_RADIUS)
167 .filter(|c| {
168 first_valid_start
169 .as_ref()
170 .is_none_or(|s| s.frame2 <= c.frame1)
171 })
172 .filter(|c| {
173 first_valid_checkpoint1
174 .as_ref()
175 .is_none_or(|cp1| cp1.frame2 <= c.frame1)
176 })
177 .filter(|c| {
178 first_valid_finish
179 .as_ref()
180 .is_none_or(|f| c.frame2 <= f.frame2)
181 })
182 .count();
183
184 let start_plane_diff_ms =
185 get_ms_diff(analyze_result, &first_valid_start, "Start_Point");
186 let checkpoint1_diff_ms =
187 get_ms_diff(analyze_result, &first_valid_checkpoint1, "Check_Point_1");
188 let finish_point_diff_ms =
189 get_ms_diff(analyze_result, &first_valid_finish, "Finish_Point");
190
191 let mut score = 0;
192
193 if nearest_start_dist <= NEAREST_START_POINT_DIST {
195 score += 1;
196 }
197 if start_plane_diff_ms.is_some_and(|d| d.abs() <= 10.0) {
198 score += 1;
199 }
200 if checkpoint1_diff_ms.is_some_and(|d| d.abs() <= 10.0) {
201 score += 1;
202 }
203 if finish_point_diff_ms.is_some_and(|d| d.abs() <= 10.0) {
204 score += 1;
205 }
206 if valid_checkpoint2_count > 0 {
207 score += 1;
208 }
209
210 LevelScore {
211 name: lpc.name.clone(),
212 score,
213 }
214 })
215 .collect();
216
217 level_scores.sort_by_key(|b| std::cmp::Reverse(b.score));
218 level_scores
219}
220
221fn get_valid_intersections(
222 lpc: &LevelPlaneIntersection,
223 object_name: &str,
224) -> Vec<PlaneIntersectionInfo> {
225 lpc.intersections
226 .iter()
227 .filter(|c| c.object_name == object_name && c.distance <= PLANE_RADIUS)
228 .cloned()
229 .collect()
230}
231
232fn get_ms_diff(
233 analyze_result: &AnalyzeResult,
234 intersection: &Option<PlaneIntersectionInfo>,
235 object_name: &str,
236) -> Option<f32> {
237 let intersection = intersection.as_ref()?;
238 #[allow(clippy::cast_precision_loss)]
239 let frame_ms = intersection.frame2 as f32 * 10.0;
240
241 match object_name {
242 "Start_Point" => Some(frame_ms - analyze_result.checkpoint1_ms), "Check_Point_1" => Some(frame_ms - analyze_result.checkpoint1_ms),
244 "Finish_Point" => Some(frame_ms - (analyze_result.displayed_ms + analyze_result.start_ms)),
245 _ => None,
246 }
247}
248
249fn get_nearest_player_start_location(
250 level_name: &str,
251 rows: &[RowData],
252 level_data: &[Level],
253) -> f32 {
254 if rows.is_empty() {
255 return f32::MAX;
256 }
257
258 let level = match level_data.iter().find(|l| l.name == level_name) {
259 Some(l) => l,
260 None => return f32::MAX,
261 };
262
263 let start_locations: Vec<&Position> = level
264 .entries
265 .iter()
266 .filter(|e| e.name == "Player_Start_Location")
267 .map(|e| &e.position)
268 .collect();
269
270 if start_locations.is_empty() {
271 return f32::MAX;
272 }
273
274 let replay_start_pos = Position {
275 x: rows[0].x,
276 y: rows[0].y,
277 z: rows[0].z,
278 };
279
280 start_locations
281 .iter()
282 .map(|start_pos| calculate_distance(&replay_start_pos, start_pos))
283 .min_by(|a, b| a.total_cmp(b))
284 .unwrap_or(f32::MAX)
285}
286
287fn calculate_distance(p1: &Position, p2: &Position) -> f32 {
288 let dx = p2.x - p1.x;
289 let dy = p2.y - p1.y;
290 let dz = p2.z - p1.z;
291 (dx * dx + dy * dy + dz * dz).sqrt()
292}