mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-01 18:03:19 -05:00
335 lines
13 KiB
Rust
335 lines
13 KiB
Rust
//! Java Edition Anvil format world saving.
|
|
//!
|
|
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
|
|
|
|
use super::common::{Chunk, ChunkToModify, Section};
|
|
use super::WorldEditor;
|
|
use crate::block_definitions::GRASS_BLOCK;
|
|
use crate::progress::emit_gui_progress_update;
|
|
use colored::Colorize;
|
|
use fastanvil::Region;
|
|
use fastnbt::Value;
|
|
use fnv::FnvHashMap;
|
|
use indicatif::{ProgressBar, ProgressStyle};
|
|
use rayon::prelude::*;
|
|
use std::collections::HashMap;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::OnceLock;
|
|
|
|
/// Cached base chunk sections (grass at Y=-62)
|
|
/// Computed once on first use and reused for all empty chunks
|
|
static BASE_CHUNK_SECTIONS: OnceLock<Vec<Section>> = OnceLock::new();
|
|
|
|
/// Get or create the cached base chunk sections
|
|
fn get_base_chunk_sections() -> &'static [Section] {
|
|
BASE_CHUNK_SECTIONS.get_or_init(|| {
|
|
let mut chunk = ChunkToModify::default();
|
|
for x in 0..16 {
|
|
for z in 0..16 {
|
|
chunk.set_block(x, -62, z, GRASS_BLOCK);
|
|
}
|
|
}
|
|
chunk.sections().collect()
|
|
})
|
|
}
|
|
|
|
#[cfg(feature = "gui")]
|
|
use crate::telemetry::{send_log, LogLevel};
|
|
|
|
impl<'a> WorldEditor<'a> {
|
|
/// Creates a region file for the given region coordinates.
|
|
pub(super) fn create_region(&self, region_x: i32, region_z: i32) -> Region<File> {
|
|
let region_dir = self.world_dir.join("region");
|
|
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
|
|
|
// Ensure region directory exists before creating region files
|
|
std::fs::create_dir_all(®ion_dir).expect("Failed to create region directory");
|
|
|
|
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
|
|
|
|
let mut region_file: File = File::options()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(&out_path)
|
|
.expect("Failed to open region file");
|
|
|
|
region_file
|
|
.write_all(REGION_TEMPLATE)
|
|
.expect("Could not write region template");
|
|
|
|
Region::from_stream(region_file).expect("Failed to load region")
|
|
}
|
|
|
|
/// Helper function to create a base chunk with grass blocks at Y -62
|
|
/// Uses cached sections for efficiency - only serialization happens per chunk
|
|
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
|
// Use cached sections (computed once on first call)
|
|
let sections = get_base_chunk_sections();
|
|
|
|
// Prepare chunk data with cloned sections
|
|
let chunk_data = Chunk {
|
|
sections: sections.to_vec(),
|
|
x_pos: abs_chunk_x,
|
|
z_pos: abs_chunk_z,
|
|
is_light_on: 0,
|
|
other: FnvHashMap::default(),
|
|
};
|
|
|
|
// Create the Level wrapper
|
|
let level_data = create_level_wrapper(&chunk_data);
|
|
|
|
// Serialize the chunk with Level wrapper
|
|
let mut ser_buffer = Vec::with_capacity(8192);
|
|
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
|
|
|
(ser_buffer, true)
|
|
}
|
|
|
|
/// Saves the world in Java Edition Anvil format.
|
|
///
|
|
/// Uses parallel processing with rayon for fast region saving.
|
|
pub(super) fn save_java(&mut self) {
|
|
println!("{} Saving world...", "[7/7]".bold());
|
|
emit_gui_progress_update(90.0, "Saving world...");
|
|
|
|
// Save metadata with error handling
|
|
if let Err(e) = self.save_metadata() {
|
|
eprintln!("Failed to save world metadata: {}", e);
|
|
#[cfg(feature = "gui")]
|
|
send_log(LogLevel::Warning, "Failed to save world metadata.");
|
|
// Continue with world saving even if metadata fails
|
|
}
|
|
|
|
let total_regions = self.world.regions.len() as u64;
|
|
let save_pb = ProgressBar::new(total_regions);
|
|
save_pb.set_style(
|
|
ProgressStyle::default_bar()
|
|
.template(
|
|
"{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})",
|
|
)
|
|
.unwrap()
|
|
.progress_chars("█▓░"),
|
|
);
|
|
|
|
let regions_processed = AtomicU64::new(0);
|
|
|
|
self.world
|
|
.regions
|
|
.par_iter()
|
|
.for_each(|((region_x, region_z), region_to_modify)| {
|
|
self.save_single_region(*region_x, *region_z, region_to_modify);
|
|
|
|
// Update progress
|
|
let regions_done = regions_processed.fetch_add(1, Ordering::SeqCst) + 1;
|
|
|
|
// Update progress at regular intervals (every ~10% or at least every 10 regions)
|
|
let update_interval = (total_regions / 10).max(1);
|
|
if regions_done.is_multiple_of(update_interval) || regions_done == total_regions {
|
|
let progress = 90.0 + (regions_done as f64 / total_regions as f64) * 9.0;
|
|
emit_gui_progress_update(progress, "Saving world...");
|
|
}
|
|
|
|
save_pb.inc(1);
|
|
});
|
|
|
|
save_pb.finish();
|
|
}
|
|
|
|
/// Saves a single region to disk.
|
|
///
|
|
/// Optimized for new world creation, writes chunks directly without reading existing data.
|
|
/// This assumes we're creating a fresh world, not modifying an existing one.
|
|
fn save_single_region(
|
|
&self,
|
|
region_x: i32,
|
|
region_z: i32,
|
|
region_to_modify: &super::common::RegionToModify,
|
|
) {
|
|
let mut region = self.create_region(region_x, region_z);
|
|
let mut ser_buffer = Vec::with_capacity(8192);
|
|
|
|
// First pass: write all chunks that have content
|
|
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
|
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
|
// Create chunk directly, we're writing to a fresh region file
|
|
// so there's no existing data to preserve
|
|
let chunk = Chunk {
|
|
sections: chunk_to_modify.sections().collect(),
|
|
x_pos: chunk_x + (region_x * 32),
|
|
z_pos: chunk_z + (region_z * 32),
|
|
is_light_on: 0,
|
|
other: chunk_to_modify.other.clone(),
|
|
};
|
|
|
|
// Create Level wrapper and save
|
|
let level_data = create_level_wrapper(&chunk);
|
|
ser_buffer.clear();
|
|
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
|
region
|
|
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
// Second pass: ensure all chunks exist (fill with base layer if not)
|
|
for chunk_x in 0..32 {
|
|
for chunk_z in 0..32 {
|
|
let abs_chunk_x = chunk_x + (region_x * 32);
|
|
let abs_chunk_z = chunk_z + (region_z * 32);
|
|
|
|
// Check if chunk exists in our modifications
|
|
let chunk_exists = region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
|
|
|
|
// If chunk doesn't exist, create it with base layer
|
|
if !chunk_exists {
|
|
let (ser_buffer, _) = Self::create_base_chunk(abs_chunk_x, abs_chunk_z);
|
|
region
|
|
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to get entity coordinates
|
|
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
|
#[inline]
|
|
#[allow(dead_code)]
|
|
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
|
if let Some(Value::List(pos)) = entity.get("Pos") {
|
|
if pos.len() == 3 {
|
|
if let (Some(x), Some(y), Some(z)) = (
|
|
value_to_i32(&pos[0]),
|
|
value_to_i32(&pos[1]),
|
|
value_to_i32(&pos[2]),
|
|
) {
|
|
return Some((x, y, z));
|
|
}
|
|
}
|
|
}
|
|
|
|
let (Some(x), Some(y), Some(z)) = (
|
|
entity.get("x").and_then(value_to_i32),
|
|
entity.get("y").and_then(value_to_i32),
|
|
entity.get("z").and_then(value_to_i32),
|
|
) else {
|
|
return None;
|
|
};
|
|
|
|
Some((x, y, z))
|
|
}
|
|
|
|
/// Creates a Level wrapper for chunk data (Java Edition format)
|
|
#[inline]
|
|
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
|
let mut level_map = HashMap::from([
|
|
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
|
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
|
(
|
|
"isLightOn".to_string(),
|
|
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
|
),
|
|
(
|
|
"sections".to_string(),
|
|
Value::List(
|
|
chunk
|
|
.sections
|
|
.iter()
|
|
.map(|section| {
|
|
let mut block_states = HashMap::from([(
|
|
"palette".to_string(),
|
|
Value::List(
|
|
section
|
|
.block_states
|
|
.palette
|
|
.iter()
|
|
.map(|item| {
|
|
let mut palette_item = HashMap::from([(
|
|
"Name".to_string(),
|
|
Value::String(item.name.clone()),
|
|
)]);
|
|
if let Some(props) = &item.properties {
|
|
palette_item
|
|
.insert("Properties".to_string(), props.clone());
|
|
}
|
|
Value::Compound(palette_item)
|
|
})
|
|
.collect(),
|
|
),
|
|
)]);
|
|
|
|
// Only add the `data` attribute if it's non-empty
|
|
// to maintain compatibility with third-party tools like Dynmap
|
|
if let Some(data) = §ion.block_states.data {
|
|
if !data.is_empty() {
|
|
block_states
|
|
.insert("data".to_string(), Value::LongArray(data.to_owned()));
|
|
}
|
|
}
|
|
|
|
Value::Compound(HashMap::from([
|
|
("Y".to_string(), Value::Byte(section.y)),
|
|
("block_states".to_string(), Value::Compound(block_states)),
|
|
]))
|
|
})
|
|
.collect(),
|
|
),
|
|
),
|
|
]);
|
|
|
|
for (key, value) in &chunk.other {
|
|
level_map.insert(key.clone(), value.clone());
|
|
}
|
|
|
|
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
|
|
}
|
|
|
|
/// Merge compound lists (entities, block_entities) from chunk_to_modify into chunk
|
|
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
|
#[allow(dead_code)]
|
|
fn merge_compound_list(chunk: &mut Chunk, chunk_to_modify: &ChunkToModify, key: &str) {
|
|
if let Some(existing_entities) = chunk.other.get_mut(key) {
|
|
if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
|
if let (Value::List(existing), Value::List(new)) = (existing_entities, new_entities) {
|
|
existing.retain(|e| {
|
|
if let Value::Compound(map) = e {
|
|
if let Some((x, y, z)) = get_entity_coords(map) {
|
|
return !new.iter().any(|new_e| {
|
|
if let Value::Compound(new_map) = new_e {
|
|
get_entity_coords(new_map) == Some((x, y, z))
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
}
|
|
}
|
|
true
|
|
});
|
|
existing.extend(new.clone());
|
|
}
|
|
}
|
|
} else if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
|
chunk.other.insert(key.to_string(), new_entities.clone());
|
|
}
|
|
}
|
|
|
|
/// Convert NBT Value to i32
|
|
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
|
#[allow(dead_code)]
|
|
fn value_to_i32(value: &Value) -> Option<i32> {
|
|
match value {
|
|
Value::Byte(v) => Some(i32::from(*v)),
|
|
Value::Short(v) => Some(i32::from(*v)),
|
|
Value::Int(v) => Some(*v),
|
|
Value::Long(v) => i32::try_from(*v).ok(),
|
|
Value::Float(v) => Some(*v as i32),
|
|
Value::Double(v) => Some(*v as i32),
|
|
_ => None,
|
|
}
|
|
}
|