Add Litematica exporter and integrate serde for serialization
- Introduced Litematica exporter with necessary data structures for Minecraft text generation. - Added serde dependency for serialization support in multiple crates. - Updated Cargo.toml files to include new dependencies and features. - Created palette module for block palette management.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
mod litematica;
|
||||
mod mcfunction;
|
||||
|
||||
pub use mcfunction::McFunctionExporter;
|
||||
|
||||
181
crates/exporters/src/litematica.rs
Normal file
181
crates/exporters/src/litematica.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::Write,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use lib::{StructureExporter, VoxelType};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Litematic {
|
||||
pub metadata: Metadata,
|
||||
pub minecraft_data_version: i32,
|
||||
pub version: i32,
|
||||
pub regions: std::collections::HashMap<String, Region>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Metadata {
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub enclosing_size: Vector3,
|
||||
pub name: String,
|
||||
pub region_count: i32,
|
||||
pub time_created: i64,
|
||||
pub time_modified: i64,
|
||||
pub total_blocks: i32,
|
||||
pub total_volume: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Region {
|
||||
pub block_state_palette: Vec<BlockState>,
|
||||
pub block_states: Vec<i64>,
|
||||
pub position: Vector3,
|
||||
pub size: Vector3,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct BlockState {
|
||||
pub name: String,
|
||||
// Optional properties like facing=north would go here,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Copy)]
|
||||
pub struct Vector3 {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub z: i32,
|
||||
}
|
||||
|
||||
pub struct LitematicaExporter {
|
||||
palette_map: HashMap<VoxelType, String>,
|
||||
}
|
||||
|
||||
impl LitematicaExporter {
|
||||
pub fn new(body_block: &str, outline_block: &str) -> Self {
|
||||
let mut palette_map = HashMap::new();
|
||||
palette_map.insert(VoxelType::Body, body_block.to_string());
|
||||
palette_map.insert(VoxelType::Outline, outline_block.to_string());
|
||||
Self { palette_map }
|
||||
}
|
||||
|
||||
fn pack_bits(indicies: &[usize], bits_per_entry: usize) -> Vec<i64> {
|
||||
let entries_per_long = 64 / bits_per_entry;
|
||||
let mut packed = Vec::new();
|
||||
let mut current_long: i64 = 0;
|
||||
let mut current_idx = 0;
|
||||
|
||||
for &index in indicies {
|
||||
current_long |= (index as i64) << (current_idx * bits_per_entry);
|
||||
current_idx += 1;
|
||||
|
||||
if current_idx >= entries_per_long {
|
||||
packed.push(current_long as i64);
|
||||
current_long = 0;
|
||||
current_idx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if current_idx > 0 {
|
||||
packed.push(current_long as i64);
|
||||
}
|
||||
|
||||
packed
|
||||
}
|
||||
}
|
||||
|
||||
impl StructureExporter for LitematicaExporter {
|
||||
fn export(&self, grid: &lib::VoxelGrid) -> anyhow::Result<Vec<u8>> {
|
||||
let mut palette_list = vec![BlockState {
|
||||
name: "minecraft:air".to_string(),
|
||||
}];
|
||||
let mut type_to_index = HashMap::new();
|
||||
|
||||
for (voxel_type, block_id) in &self.palette_map {
|
||||
type_to_index.insert(*voxel_type, palette_list.len());
|
||||
palette_list.push(BlockState {
|
||||
name: block_id.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let bits_per_entry =
|
||||
std::cmp::max(2, (palette_list.len() as f64 - 1.0).log2().ceil() as usize);
|
||||
|
||||
let mut indices = Vec::with_capacity((grid.width * grid.height * grid.depth) as usize);
|
||||
let mut total_blocks = 0;
|
||||
|
||||
for y in 0..grid.height {
|
||||
for z in 0..grid.depth {
|
||||
for x in 0..grid.width {
|
||||
if let Some(voxel) = grid.get(x, y, z) {
|
||||
indices.push(*type_to_index.get(&voxel).unwrap());
|
||||
total_blocks += 1;
|
||||
} else {
|
||||
indices.push(0); // Index 0 is Air
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let packed_states = Self::pack_bits(&indices, bits_per_entry);
|
||||
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as i64;
|
||||
let dimensions = Vector3 {
|
||||
x: grid.width as i32,
|
||||
y: grid.height as i32,
|
||||
z: grid.depth as i32,
|
||||
};
|
||||
|
||||
let mut regions = HashMap::new();
|
||||
regions.insert(
|
||||
"TextRegion".to_string(),
|
||||
Region {
|
||||
block_state_palette: palette_list,
|
||||
block_states: packed_states,
|
||||
position: Vector3 { x: 0, y: 0, z: 0 },
|
||||
size: dimensions,
|
||||
},
|
||||
);
|
||||
|
||||
let litematic = Litematic {
|
||||
minecraft_data_version: 3465, // 1.20+ Data Version
|
||||
version: 6, // Litematica format version
|
||||
metadata: Metadata {
|
||||
author: "Minecraft Text Builder".to_string(),
|
||||
description: "Generated 3D Text".to_string(),
|
||||
name: "Text_Structure".to_string(),
|
||||
enclosing_size: dimensions,
|
||||
region_count: 1,
|
||||
total_blocks,
|
||||
total_volume: (grid.width * grid.height * grid.depth) as i32,
|
||||
time_created: timestamp,
|
||||
time_modified: timestamp,
|
||||
},
|
||||
regions,
|
||||
};
|
||||
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
|
||||
let nbt_bytes = fastnbt::to_bytes(&litematic).context("NBT serialization failed")?;
|
||||
|
||||
encoder.write_all(&nbt_bytes)?;
|
||||
|
||||
Ok(encoder.finish()?)
|
||||
}
|
||||
|
||||
fn file_extension(&self) -> &'static str {
|
||||
"litematic"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user