1use fastanvil::Region;
4use rayon::prelude::{IntoParallelIterator, ParallelIterator};
5use rayon::{ThreadPoolBuildError, ThreadPoolBuilder};
6use serde::{Deserialize, Serialize};
7use std::fs::File;
8use std::io::{self, Seek};
9use std::path::{Path, PathBuf};
10use std::sync::atomic::AtomicU64;
11use std::sync::mpsc;
12use std::time::Duration;
13use std::{fs, thread, time};
14
15const REGION_SUBFOLDERS: [&str; 3] = ["region", "DIM-1/region", "DIM1/region"];
17
18#[derive(Default)]
20pub struct Config {
21 pub world_folder: PathBuf,
23 pub max_inhabited_time: usize,
25 pub thread_count: usize,
27}
28
29#[derive(Serialize)]
31pub struct Report {
32 pub time_taken: Duration,
34 pub total_freed_space: u64,
36 pub total_regions: u64,
38 pub total_chunks: u64,
40 pub total_deleted_chunks: u64,
42}
43
44#[derive(thiserror::Error, Debug)]
46pub enum Error {
47 #[error("The specified world folder could not be found")]
49 WorldFolderNotFound,
50 #[error("Unknown IO error")]
52 IOError(#[from] io::Error),
53 #[error("Failed to build Rayon threadpool")]
55 RayonError(#[from] ThreadPoolBuildError),
56}
57
58pub enum ProcessingUpdate {
60 Starting {
62 total_files: u64,
64 },
65 ProcessedRegion(Result<ProcessedRegion, RegionProcessingError>),
68 Finished(Report),
70}
71
72pub fn execute(config: Config) -> Result<mpsc::Receiver<ProcessingUpdate>, Error> {
76 if !config.world_folder.try_exists().map_or(false, |r| r) {
77 return Err(Error::WorldFolderNotFound);
78 }
79
80 ThreadPoolBuilder::new()
81 .num_threads(config.thread_count)
82 .build_global()?;
83
84 let (tx, rx) = mpsc::channel();
85
86 let files = collect_region_files(Path::new(&config.world_folder))?;
87
88 let size_before = dir_size(config.world_folder.as_path())?;
89 let start_time = time::Instant::now();
90 let total_regions = files.len() as u64;
91 let total_chunks = AtomicU64::new(0);
92 let total_deleted_chunks = AtomicU64::new(0);
93
94 thread::spawn(move || {
95 let _ = tx.send(ProcessingUpdate::Starting {
96 total_files: files.len() as u64,
97 });
98
99 let result = files
100 .into_par_iter()
101 .try_for_each_with(tx.clone(), |t, path| {
102 let processed_region =
103 process_region_file(path.as_path(), config.max_inhabited_time * 20);
104
105 if let Ok(ProcessedRegion {
106 x: _,
107 y: _,
108 total_chunks: chunks,
109 deleted_chunks,
110 }) = processed_region
111 {
112 total_chunks.fetch_add(chunks as u64, std::sync::atomic::Ordering::Relaxed);
113 total_deleted_chunks
114 .fetch_add(deleted_chunks as u64, std::sync::atomic::Ordering::Relaxed);
115 }
116
117 if t.send(ProcessingUpdate::ProcessedRegion(processed_region))
118 .is_err()
119 {
120 Err(())
121 } else {
122 Ok(())
123 }
124 });
125 if result.is_ok() {
126 let freed_space = size_before - dir_size(config.world_folder.as_path()).unwrap_or(0);
127 let time_taken = time::Instant::now() - start_time;
128
129 let _ = tx.send(ProcessingUpdate::Finished(Report {
130 time_taken,
131 total_freed_space: freed_space,
132 total_regions,
133 total_chunks: total_chunks.into_inner(),
134 total_deleted_chunks: total_deleted_chunks.into_inner(),
135 }));
136 }
137 });
138
139 Ok(rx)
140}
141
142fn collect_region_files(base_path: &Path) -> io::Result<Vec<PathBuf>> {
143 let mut files = vec![];
144 for sub_folder in REGION_SUBFOLDERS {
145 let path = base_path.join(Path::new(sub_folder));
146 if !path.try_exists().map_or(false, |b| b) {
147 continue;
148 }
149 let mut contents = path
150 .read_dir()?
151 .map(|entry| entry.unwrap().path())
152 .filter(|path| {
153 if let Some(ext) = path.extension() {
154 ext == "mca"
155 } else {
156 false
157 }
158 })
159 .collect();
160 files.append(&mut contents);
161 }
162 Ok(files)
163}
164
165#[derive(thiserror::Error, Debug)]
167pub enum RegionProcessingError {
168 #[error("Unknown I/O error")]
170 IOError(#[from] io::Error),
171 #[error("Anvil error")]
173 AnvilError(#[from] fastanvil::Error),
174 #[error("NBT error")]
176 NBTError(#[from] fastnbt::error::Error),
177}
178
179#[derive(Serialize, Deserialize)]
180#[serde(rename_all = "PascalCase")]
181struct Chunk {
182 inhabited_time: usize,
183}
184
185pub struct ProcessedRegion {
187 pub x: usize,
189 pub y: usize,
191 pub total_chunks: u16,
193 pub deleted_chunks: u16,
195}
196
197fn process_region_file(
198 region_file_path: &Path,
199 man_inhabited_time: usize,
200) -> Result<ProcessedRegion, RegionProcessingError> {
201 let mut total_chunks = 0;
202 let mut deleted_chunks = 0;
203
204 let (y, x) = match region_file_path
205 .file_stem()
206 .and_then(|os| os.to_str())
207 .map(|s| s.split('.').skip(1).collect::<Vec<_>>())
208 {
209 Some(mut vec) => (
210 vec.pop().unwrap_or("0").parse::<usize>().unwrap_or(0),
211 vec.pop().unwrap_or("0").parse::<usize>().unwrap_or(0),
212 ),
213 None => (0, 0),
214 };
215
216 let region_file = File::options()
217 .read(true)
218 .write(true)
219 .open(region_file_path)?;
220 let mut region = Region::from_stream(region_file)?;
221
222 for x in 0..32 {
223 for y in 0..32 {
224 let Ok(Some(chunk)) = region.read_chunk(x, y) else { continue; };
225 let chunk: Chunk = fastnbt::from_bytes(&chunk)?;
226 total_chunks += 1;
227 if chunk.inhabited_time <= (man_inhabited_time / 20) {
228 region.remove_chunk(x, y)?;
229 deleted_chunks += 1;
230 }
231 }
232 }
233
234 let mut region_file = region.into_inner()?;
236 let len = region_file.stream_position()?;
237 region_file.set_len(len)?;
238
239 Ok(ProcessedRegion {
240 x,
241 y,
242 total_chunks,
243 deleted_chunks,
244 })
245}
246
247fn dir_size(path: &Path) -> io::Result<u64> {
249 fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
250 dir.try_fold(0, |acc, file| {
251 let file = file?;
252 let size = match file.metadata()? {
253 data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
254 data => data.len(),
255 };
256 Ok(acc + size)
257 })
258 }
259
260 dir_size(fs::read_dir(path)?)
261}