wows_data_mgr/
constants.rs1use rootcause::prelude::*;
7
8#[derive(Debug, Clone, serde::Deserialize)]
11pub struct ConstantsVersion {
12 pub version: String,
13 #[serde(default)]
14 pub patch: f64,
15}
16
17impl ConstantsVersion {
18 pub fn friendly_version(&self) -> String {
20 format!("{}.{}", self.version, self.patch as i64)
21 }
22}
23
24pub async fn fetch_constants_manifest() -> Option<std::collections::BTreeMap<u32, ConstantsVersion>> {
26 use http_body_util::BodyExt;
27 use octocrab::params::repos::Reference;
28
29 let response = octocrab::instance()
30 .repos("padtrack", "wows-constants")
31 .raw_file(Reference::Branch("main".to_string()), "manifest.json")
32 .await
33 .ok()?;
34
35 let mut body = response.into_body();
36 let mut result = Vec::new();
37
38 while let Some(frame) = body.frame().await {
39 match frame {
40 Ok(frame) => {
41 if let Some(data) = frame.data_ref() {
42 result.extend_from_slice(data);
43 }
44 }
45 Err(_) => return None,
46 }
47 }
48
49 let raw: std::collections::BTreeMap<String, ConstantsVersion> = serde_json::from_slice(&result).ok()?;
51 Some(raw.into_iter().filter_map(|(k, v)| k.parse::<u32>().ok().map(|b| (b, v))).collect())
52}
53
54pub fn resolve_manifest_build(
58 target_build: u32,
59 target_version: Option<&str>,
60 manifest: &std::collections::BTreeMap<u32, ConstantsVersion>,
61) -> Option<u32> {
62 if manifest.contains_key(&target_build) {
63 return Some(target_build);
64 }
65 let want = target_version?;
66 manifest.iter().filter(|(_, v)| v.friendly_version() == want).map(|(b, _)| *b).max()
67}
68
69pub fn fetch_versioned_constants_blocking(
75 build: u32,
76 target_version: Option<&str>,
77) -> Result<(serde_json::Value, u32), rootcause::Report> {
78 let runtime = tokio::runtime::Builder::new_current_thread()
79 .enable_all()
80 .build()
81 .attach_with(|| "Failed to create tokio runtime")?;
82
83 runtime.block_on(fetch_versioned_constants(build, target_version))
84}
85
86pub async fn fetch_versioned_constants(
89 target_build: u32,
90 target_version: Option<&str>,
91) -> Result<(serde_json::Value, u32), rootcause::Report> {
92 if let Some(manifest) = fetch_constants_manifest().await
93 && let Some(resolved) = resolve_manifest_build(target_build, target_version, &manifest)
94 && let Some(data) = fetch_build(resolved).await
95 {
96 return Ok((data, resolved));
97 }
98 let available = list_available_builds().await?;
100 pick_constants(target_build, &available)
101 .await
102 .ok_or_else(|| report!("No constants found for build {target_build} or any older build"))
103}
104
105async fn pick_constants(target_build: u32, available: &[u32]) -> Option<(serde_json::Value, u32)> {
109 if available.contains(&target_build)
110 && let Some(data) = fetch_build(target_build).await
111 {
112 return Some((data, target_build));
113 }
114
115 for &build in available.iter().rev() {
116 if build >= target_build {
117 continue;
118 }
119 if let Some(data) = fetch_build(build).await {
120 return Some((data, build));
121 }
122 }
123 None
124}
125
126pub struct ConstantsFetcher {
130 runtime: tokio::runtime::Runtime,
131 manifest: Option<std::collections::BTreeMap<u32, ConstantsVersion>>,
132 available: Vec<u32>,
133}
134
135impl ConstantsFetcher {
136 pub fn new() -> Result<Self, rootcause::Report> {
138 let runtime = tokio::runtime::Builder::new_current_thread()
139 .enable_all()
140 .build()
141 .attach_with(|| "Failed to create tokio runtime")?;
142 let manifest = runtime.block_on(fetch_constants_manifest());
143 let available = runtime.block_on(list_available_builds())?;
144 Ok(Self { runtime, manifest, available })
145 }
146
147 pub fn fetch(&self, target_build: u32, target_version: Option<&str>) -> Option<(serde_json::Value, u32)> {
151 if let Some(manifest) = self.manifest.as_ref()
152 && let Some(resolved) = resolve_manifest_build(target_build, target_version, manifest)
153 && let Some(data) = self.runtime.block_on(fetch_build(resolved))
154 {
155 return Some((data, resolved));
156 }
157 self.runtime.block_on(pick_constants(target_build, &self.available))
158 }
159}
160
161pub async fn list_available_builds() -> Result<Vec<u32>, rootcause::Report> {
163 let items = octocrab::instance()
164 .repos("padtrack", "wows-constants")
165 .get_content()
166 .path("data/versions")
167 .r#ref("main")
168 .send()
169 .await
170 .attach_with(|| "Failed to list constants builds from GitHub")?;
171
172 let mut builds: Vec<u32> =
173 items.items.iter().filter_map(|item| item.name.strip_suffix(".json")?.parse::<u32>().ok()).collect();
174 builds.sort();
175 Ok(builds)
176}
177
178pub async fn fetch_build(build: u32) -> Option<serde_json::Value> {
180 use http_body_util::BodyExt;
181 use octocrab::params::repos::Reference;
182
183 let path = format!("data/versions/{build}.json");
184 let response = octocrab::instance()
185 .repos("padtrack", "wows-constants")
186 .raw_file(Reference::Branch("main".to_string()), &path)
187 .await
188 .ok()?;
189
190 let mut body = response.into_body();
191 let mut result = Vec::new();
192
193 while let Some(frame) = body.frame().await {
194 match frame {
195 Ok(frame) => {
196 if let Some(data) = frame.data_ref() {
197 result.extend_from_slice(data);
198 }
199 }
200 Err(_) => return None,
201 }
202 }
203
204 serde_json::from_slice(&result).ok()
205}
206
207#[cfg(test)]
208mod manifest_tests {
209 use std::collections::BTreeMap;
210
211 use super::*;
212 fn m() -> BTreeMap<u32, ConstantsVersion> {
213 let mut m = BTreeMap::new();
214 m.insert(11965230, ConstantsVersion { version: "15.1".into(), patch: 0.0 });
215 m.insert(12506899, ConstantsVersion { version: "15.4".into(), patch: 0.0 });
216 m
217 }
218 #[test]
219 fn friendly_version_reconstructs() {
220 assert_eq!(ConstantsVersion { version: "15.4".into(), patch: 0.0 }.friendly_version(), "15.4.0");
221 }
222 #[test]
223 fn exact_build_wins() {
224 assert_eq!(resolve_manifest_build(12506899, Some("15.4.0"), &m()), Some(12506899));
225 }
226 #[test]
227 fn cross_region_resolves_by_version() {
228 assert_eq!(resolve_manifest_build(99999999, Some("15.1.0"), &m()), Some(11965230));
230 }
231 #[test]
232 fn no_match_is_none() {
233 assert_eq!(resolve_manifest_build(99999999, Some("9.9.9"), &m()), None);
234 assert_eq!(resolve_manifest_build(99999999, None, &m()), None);
235 }
236}