poe_data_tools_cli/commands/
dump_trees.rs1use std::{
2 collections::HashMap,
3 fs::{File, create_dir_all},
4 io::BufWriter,
5 path::Path,
6};
7
8use anyhow::{Context, Result, ensure};
9use glob::{MatchOptions, Pattern};
10use poe_data_tools::{
11 Patch,
12 file_parsers::{
13 FileParser,
14 psg::{PSGParser, types::PSGFile},
15 },
16 fs::{FS, FileSystem},
17 tree::{
18 passive_info::{PassiveSkillInfo, load_passive_info},
19 psg::PassiveSkillGraph,
20 },
21};
22
23use crate::VERBOSE;
24
25fn process_file(
26 contents: &[u8],
27 output_path: &Path,
28 version: &Patch,
29 passive_info: &HashMap<u16, PassiveSkillInfo>,
30) -> Result<()> {
31 let psg_file = PSGParser {
33 version: version.major(),
34 }
35 .parse(contents)
36 .as_anyhow()
37 .context("Failed to parse passive skill tree")?;
38
39 let passive_info = {
41 let ids = psg_file
42 .groups
43 .iter()
44 .flat_map(|g| g.passives.iter().map(|p| p.id as u16));
45 ids.map(|id| (id, passive_info[&id].clone())).collect()
46 };
47
48 let passive_tree = {
49 let PSGFile {
50 version,
51 graph_type,
52 passives_per_orbit,
53 root_passives,
54 groups,
55 } = psg_file;
56
57 PassiveSkillGraph {
58 version,
59 graph_type,
60 passives_per_orbit,
61 root_passives,
62 groups,
63 passive_info,
64 }
65 };
66
67 create_dir_all(output_path.parent().context("No parent directory")?)
69 .context("Failed to create output dirs")?;
70
71 let f = File::create(output_path)
72 .with_context(|| format!("Failed to create file {:?}", output_path))?;
73 let f = BufWriter::new(f);
74
75 serde_json::to_writer_pretty(f, &passive_tree).context("Failed to serialise tree to JSON")
76}
77
78pub fn dump_trees(
79 fs: &mut FS,
80 patterns: &[Pattern],
81 output_folder: &Path,
82 version: &Patch,
83 cache_dir: &Path,
84) -> Result<()> {
85 for pattern in patterns {
86 ensure!(
87 pattern.as_str().ends_with(".psg"),
88 "Only .psg tree export is supported."
89 );
90 }
91
92 let filenames = fs
93 .list()
94 .filter(|filename| {
95 patterns.iter().any(|pattern| {
96 pattern.matches_with(
97 filename,
98 MatchOptions {
99 require_literal_separator: true,
100 ..Default::default()
101 },
102 )
103 })
104 })
105 .collect::<Vec<_>>();
106
107 let passive_info = load_passive_info(fs, version, cache_dir)?
108 .into_iter()
109 .map(|p| (p.graph_passive_id, p))
110 .collect::<HashMap<_, _>>();
111
112 fs.batch_read(&filenames)
113 .filter_map(|(path, res)| match res {
115 Ok(b) => Some((path, b)),
116 Err(e) => {
117 log::error!("Failed to extract file: {:?}: {:?}", path, e);
118 None
119 }
120 })
121 .map(|(filename, contents)| -> Result<_, anyhow::Error> {
123 let output_path = output_folder.join(filename.as_ref()).with_extension("json");
125 process_file(&contents, &output_path, version, &passive_info)
126 .with_context(|| format!("Failed to process file: {:?}", filename))?;
127
128 Ok(filename)
129 })
130 .for_each(|result| match result {
132 Ok(filename) => log::info!("Extracted tree: {}", filename),
133 Err(e) => {
134 let error_message = if *VERBOSE.get().unwrap() {
135 format!("{e:?}")
136 } else {
137 format!("{e}")
138 };
139 log::error!("Failed to extract tree: {error_message}");
140 }
141 });
142
143 Ok(())
144}