use anyhow::{Context, Result};
use std::fs::{DirEntry, File};
use std::ops::RangeInclusive;
use std::os::unix::prelude::MetadataExt;
use std::sync::mpsc::channel;
use std::sync::Arc;
use std::{cmp, fs, thread};
use crate::{BlockCoordinates, BlockStack, ExportParams};
use fastanvil::{Chunk, CurrentJavaChunk, Region};
use fastnbt::from_bytes;
const CHUNK_BLOCKS_SIZE: usize = 16;
const FILE_CHUNKS_SIZE: i32 = 32;
const FILE_BLOCKS_SIZE: i32 = CHUNK_BLOCKS_SIZE as i32 * FILE_CHUNKS_SIZE;
pub(crate) fn read_level(lvl_path: &str, params: ExportParams) -> Result<BlockStack> {
let needed_filenames = get_needed_filenames(¶ms);
let paths = fs::read_dir(lvl_path).context("Cannot read lvl dir")?;
let files: Vec<DirEntry> = paths
.into_iter()
.flatten()
.filter(|dir| {
dir.file_name().to_str().map_or(false, |filename| {
needed_filenames.contains(&filename.to_owned())
})
})
.filter(|dir| dir.metadata().map_or(false, |meta| meta.size() > 0))
.collect();
let (sender, receiver) = channel();
let export_params = Arc::new(params);
for dir_entry in files {
let p = export_params.clone();
let own_sender = sender.clone();
thread::spawn(move || {
let blocks = read_level_file(&dir_entry, &p).unwrap();
own_sender
.send(blocks)
.context("Cannot send parsed coordinates from thread")
.unwrap();
});
}
drop(sender);
let mut stack = BlockStack::default();
for blocks in receiver {
stack.add_all(blocks);
}
Ok(stack)
}
fn read_level_file(dir_entry: &DirEntry, params: &ExportParams) -> Result<Vec<BlockCoordinates>> {
let blocks_to_skip: Vec<&str> = params
.skip_blocks
.iter()
.map(std::ops::Deref::deref)
.collect();
let (filepath, filename) = (
dir_entry
.path()
.to_str()
.context("Cannot convert file path to str")?
.to_string(),
dir_entry
.file_name()
.to_str()
.context("Cannot convert file name to str")?
.to_string(),
);
let file = File::open(&filepath).context(format!("Cannot open file {}", &filepath))?;
let d = filename[2..filename.len() - 4]
.split('.')
.collect::<Vec<&str>>();
let file_x = d[0]
.parse::<i32>()
.context(format!("File {} has wrong name", &filepath))?;
let file_z = d[1]
.parse::<i32>()
.context(format!("File {} has wrong name", &filepath))?;
let (x_range, z_range) = get_chunk_xz_ranges(file_x, file_z, params);
let mut region = Region::from_stream(file).context("Cannot create region from file.")?;
let file_min_x = file_x * FILE_CHUNKS_SIZE * CHUNK_BLOCKS_SIZE as i32;
let file_min_z = file_z * FILE_CHUNKS_SIZE * CHUNK_BLOCKS_SIZE as i32;
let y_range_len = range_len_y(&(params.start.y..=params.end.y));
let mut blocks =
Vec::with_capacity(range_len(&x_range) * range_len(&z_range) * (y_range_len / 2));
for raw_chunk in region.iter().flatten() {
let mut chunk_min_x = (raw_chunk.x * CHUNK_BLOCKS_SIZE) as i32;
if file_x < 0 {
chunk_min_x = -chunk_min_x + FILE_BLOCKS_SIZE;
}
chunk_min_x += file_min_x;
let mut chunk_min_z = (raw_chunk.z * CHUNK_BLOCKS_SIZE) as i32;
if file_z < 0 {
chunk_min_z = -chunk_min_z + FILE_BLOCKS_SIZE;
}
chunk_min_z += file_min_z;
if !should_export_chunk(&x_range, &z_range, chunk_min_x, chunk_min_z) {
continue;
}
let chunk: CurrentJavaChunk =
from_bytes(raw_chunk.data.as_slice()).context("Cannot parse chunk data.")?;
for y in params.start.y..=params.end.y {
for x in 0..CHUNK_BLOCKS_SIZE {
for z in 0..CHUNK_BLOCKS_SIZE {
let block_x = chunk_min_x + x as i32;
let block_z = chunk_min_z + z as i32;
if x_range.contains(&block_x)
&& z_range.contains(&block_z)
&& chunk
.block(x, y as isize, z)
.filter(|block| {
block.name() != "minecraft:air"
&& !blocks_to_skip.contains(&block.name())
})
.is_some()
{
let point = BlockCoordinates::new(block_x, y, block_z);
blocks.push(point);
}
}
}
}
}
Ok(blocks)
}
fn should_export_chunk(
x_range: &RangeInclusive<i32>,
z_range: &RangeInclusive<i32>,
chunk_min_x: i32,
chunk_min_z: i32,
) -> bool {
let chunk_max_x = chunk_min_x + CHUNK_BLOCKS_SIZE as i32;
let chunk_max_z = chunk_min_z + CHUNK_BLOCKS_SIZE as i32;
let chunk_x_range = chunk_min_x..chunk_max_x;
let chunk_z_range = chunk_min_z..chunk_max_z;
let x_is_valid = chunk_x_range.contains(x_range.start())
|| chunk_x_range.contains(x_range.end())
|| x_range.contains(&chunk_x_range.start)
|| x_range.contains(&chunk_x_range.end);
let z_is_valid = chunk_z_range.contains(z_range.start())
|| chunk_z_range.contains(z_range.end())
|| z_range.contains(&chunk_z_range.start)
|| z_range.contains(&chunk_z_range.end);
x_is_valid && z_is_valid
}
fn get_chunk_xz_ranges(
file_x: i32,
file_z: i32,
params: &ExportParams,
) -> (RangeInclusive<i32>, RangeInclusive<i32>) {
let x_range = get_chunk_coordinate_ranges(file_x, params.start.x, params.end.x);
let z_range = get_chunk_coordinate_ranges(file_z, params.start.z, params.end.z);
(x_range, z_range)
}
fn get_chunk_coordinate_ranges(file_c: i32, start_c: i32, end_c: i32) -> RangeInclusive<i32> {
if file_c < 0 {
let min = cmp::max((file_c - 1) * FILE_BLOCKS_SIZE, start_c);
let max = cmp::min((file_c + 1) * FILE_BLOCKS_SIZE, end_c);
min..=max
} else {
let min = cmp::max(file_c * FILE_BLOCKS_SIZE, start_c);
let max = cmp::min((file_c + 1) * FILE_BLOCKS_SIZE, end_c);
min..=max
}
}
fn get_needed_filenames(params: &ExportParams) -> Vec<String> {
let mut needed_files = vec![];
let get_file_index = |c: i32| -> i32 { (c as f32 / FILE_BLOCKS_SIZE as f32).floor() as i32 };
let start_x = get_file_index(params.start.x);
let start_z = get_file_index(params.start.z);
let end_x = get_file_index(params.end.x);
let end_z = get_file_index(params.end.z);
for x in start_x..=end_x {
for z in start_z..=end_z {
needed_files.push(format!("r.{}.{}.mca", x, z));
}
}
if needed_files.is_empty() {
needed_files.push(format!("r.{}.{}.mca", start_x, start_z));
}
needed_files
}
fn range_len(range: &RangeInclusive<i32>) -> usize {
let len = if *range.start() >= 0 {
range.end() - range.start() + 1
} else if *range.end() >= 0 {
range.end() + range.start().abs() + 1
} else {
range.start().abs() + range.end() + 1
};
len as usize
}
fn range_len_y(range: &RangeInclusive<i16>) -> usize {
let len = if *range.start() >= 0 {
range.end() - range.start() + 1
} else if *range.end() >= 0 {
range.end() + range.start().abs() + 1
} else {
range.start().abs() + range.end() + 1
};
len as usize
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_level_export_range_1() {
let result = read_level(
"./assets/test_lvl",
ExportParams {
start: BlockCoordinates::new(1, -63, 1),
end: BlockCoordinates::new(2, -63, 2),
..Default::default()
},
);
assert_eq!(
result.unwrap(),
BlockStack::from(vec![
BlockCoordinates::new(1, -63, 1),
BlockCoordinates::new(1, -63, 2),
BlockCoordinates::new(2, -63, 1),
BlockCoordinates::new(2, -63, 2),
])
);
}
#[test]
fn read_level_export_range_2() {
let result = read_level(
"./assets/test_lvl",
ExportParams {
start: BlockCoordinates::new(1, -63, 5),
end: BlockCoordinates::new(2, -60, 6),
..Default::default()
},
);
assert_eq!(
result.unwrap(),
BlockStack::from(vec![
BlockCoordinates::new(1, -63, 5),
BlockCoordinates::new(1, -62, 5),
BlockCoordinates::new(1, -61, 5),
])
);
}
#[test]
fn read_level_skip_blocks_1() {
let result = read_level(
"./assets/test_lvl",
ExportParams {
start: BlockCoordinates::new(1, -63, 1),
end: BlockCoordinates::new(2, -63, 2),
skip_blocks: vec!["minecraft:stone".to_owned()],
},
);
assert_eq!(result.unwrap(), BlockStack::from(vec![]));
}
#[test]
fn read_level_skip_blocks_2() {
let result = read_level(
"./assets/test_lvl",
ExportParams {
start: BlockCoordinates::new(1, -64, 1),
end: BlockCoordinates::new(2, -63, 2),
skip_blocks: vec!["minecraft:stone".to_owned()],
},
);
assert_eq!(
result.unwrap(),
BlockStack::from(vec![
BlockCoordinates::new(1, -64, 1),
BlockCoordinates::new(1, -64, 2),
BlockCoordinates::new(2, -64, 1),
BlockCoordinates::new(2, -64, 2),
])
);
}
#[test]
fn get_chunk_ranges_1() {
assert_eq!(get_chunk_coordinate_ranges(-1, -10, -2), -10..=-2);
assert_eq!(get_chunk_coordinate_ranges(0, 0, 50), 0..=50);
assert_eq!(get_chunk_coordinate_ranges(0, -10, 1000), 0..=512);
assert_eq!(get_chunk_coordinate_ranges(1, -10, 1000), 512..=1000);
}
#[test]
fn get_needed_filenames_1() {
let result = get_needed_filenames(&ExportParams {
start: BlockCoordinates::new(-1, 0, -1),
end: BlockCoordinates::new(1, 0, 1),
..Default::default()
});
assert_eq!(
result,
vec![
String::from("r.-1.-1.mca"),
String::from("r.-1.0.mca"),
String::from("r.0.-1.mca"),
String::from("r.0.0.mca")
]
);
}
#[test]
fn get_needed_filenames_0_0() {
let result = get_needed_filenames(&ExportParams {
start: BlockCoordinates::new(1, 0, 1),
end: BlockCoordinates::new(2, 0, 2),
..Default::default()
});
assert_eq!(result, vec![String::from("r.0.0.mca")]);
}
#[test]
fn get_needed_filenames_1_0() {
let result = get_needed_filenames(&ExportParams {
start: BlockCoordinates::new(513, 1, 1),
end: BlockCoordinates::new(523, 1, 2),
..Default::default()
});
assert_eq!(result, vec![String::from("r.1.0.mca")]);
}
#[test]
fn get_needed_filenames_0_1() {
let result = get_needed_filenames(&ExportParams {
start: BlockCoordinates::new(1, 1, 513),
end: BlockCoordinates::new(2, 1, 523),
..Default::default()
});
assert_eq!(result, vec![String::from("r.0.1.mca")]);
}
#[test]
fn get_needed_filenames_1_1() {
let result = get_needed_filenames(&ExportParams {
start: BlockCoordinates::new(513, 1, 513),
end: BlockCoordinates::new(513, 1, 523),
..Default::default()
});
assert_eq!(result, vec![String::from("r.1.1.mca")]);
}
#[test]
fn get_needed_filenames_minus_2_2() {
let result = get_needed_filenames(&ExportParams {
start: BlockCoordinates::new(-513, 1, -513),
end: BlockCoordinates::new(-513, 1, -523),
..Default::default()
});
assert_eq!(result, vec![String::from("r.-2.-2.mca")]);
}
#[test]
fn range_len_1() {
assert_eq!(range_len(&(0..=5)), 6);
assert_eq!(range_len(&(-5..=5)), 11);
assert_eq!(range_len(&(-10..=-5)), 6);
}
}