use crate::{
bit_writer::BitWriter,
save::{Brick, Color, ColorMode, Direction, Rotation, User},
ue4_date_time_base, MAGIC,
};
use byteorder::{BigEndian, ByteOrder, LittleEndian, WriteBytesExt};
use chrono::prelude::*;
use libflate::zlib;
use std::{
convert::TryFrom,
io::{self, prelude::*},
};
use uuid::Uuid;
const LATEST_VERSION: u16 = 4;
pub struct WriteData {
pub map: String,
pub author: User,
pub description: String,
pub save_time: DateTime<Utc>,
pub mods: Vec<String>,
pub brick_assets: Vec<String>,
pub colors: Vec<Color>,
pub materials: Vec<String>,
pub brick_owners: Vec<User>,
pub bricks: Vec<Brick>,
}
pub fn write_save(w: &mut impl Write, data: &WriteData) -> io::Result<()> {
if data.bricks.len() > i32::max_value() as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Brick count out of range",
));
}
w.write_all(&MAGIC)?;
w.write_u16::<LittleEndian>(LATEST_VERSION)?;
let mut s = Compressed::new();
write_string(&mut s, &data.map)?;
write_string(&mut s, &data.author.name)?;
write_string(&mut s, &data.description)?;
write_uuid(&mut s, &data.author.id)?;
write_date_time(&mut s, data.save_time)?;
s.write_i32::<LittleEndian>(data.bricks.len() as i32)?;
s.finish(w)?;
let mut s = Compressed::new();
write_array(&mut s, |w, s| write_string(w, s), &data.mods)?;
write_array(&mut s, |w, s| write_string(w, s), &data.brick_assets)?;
write_array(
&mut s,
|w, c| w.write_u32::<LittleEndian>((*c).into()),
&data.colors,
)?;
write_array(&mut s, |w, s| write_string(w, s), &data.materials)?;
write_array(
&mut s,
|w, o| {
write_uuid(w, &o.id)?;
write_string(w, &o.name)
},
&data.brick_owners,
)?;
s.finish(w)?;
assert!(data.brick_assets.len() <= u32::max_value() as usize);
assert!(data.colors.len() <= u32::max_value() as usize);
let mut s = BitWriter::new(Compressed::new());
for brick in &data.bricks {
s.byte_align()?;
s.write_int(
brick.asset_name_index,
data.brick_assets.len().max(2) as u32,
)?;
if s.write_bit(brick.size != (0, 0, 0))? {
s.write_positive_int_vector_packed(brick.size)?;
}
s.write_int_vector_packed(brick.position)?;
let orientation = combine_orientation(brick.direction, brick.rotation);
s.write_int(u32::from(orientation), 24)?;
s.write_bit(brick.collision)?;
s.write_bit(brick.visibility)?;
if s.write_bit(brick.material_index != 1)? {
s.write_int_packed(brick.material_index)?;
}
match brick.color {
ColorMode::Set(i) => {
s.write_bit(false)?;
s.write_int(i, data.colors.len() as u32)?;
}
ColorMode::Custom(c) => {
s.write_bit(true)?;
s.write_u32::<LittleEndian>(c.into())?;
}
}
s.write_int_packed(brick.owner_index)?;
}
s.finish()?.finish(w)?;
Ok(())
}
struct Compressed {
encoder: zlib::Encoder<Vec<u8>>,
uncompressed: Vec<u8>,
}
impl Compressed {
fn new() -> Self {
let encoder = zlib::Encoder::new(vec![]).unwrap();
Self {
encoder,
uncompressed: vec![],
}
}
fn finish(self, w: &mut impl Write) -> io::Result<()> {
let compressed = self.encoder.finish().into_result()?;
let uncompressed_size = self.uncompressed.len();
let compressed_size = compressed.len();
if uncompressed_size >= i32::max_value() as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"uncompressed_size out of range",
));
}
if compressed_size >= i32::max_value() as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"compressed_size out of range",
));
}
let uncompressed_size = uncompressed_size as i32;
let compressed_size = compressed_size as i32;
w.write_i32::<LittleEndian>(uncompressed_size)?;
if compressed_size >= uncompressed_size {
w.write_i32::<LittleEndian>(0)?;
w.write_all(&self.uncompressed)
} else {
w.write_i32::<LittleEndian>(compressed_size)?;
w.write_all(&compressed)
}
}
}
impl Write for Compressed {
fn write(&mut self, src: &[u8]) -> io::Result<usize> {
let written = self.encoder.write(src)?;
self.uncompressed.extend(&src[..written]);
Ok(written)
}
fn flush(&mut self) -> io::Result<()> {
unimplemented!()
}
}
fn write_array<T, W: Write>(
w: &mut W,
write: impl Fn(&mut W, &T) -> io::Result<()>,
values: &[T],
) -> io::Result<()> {
if values.len() > i32::max_value() as usize {
return Err(io::Error::from(io::ErrorKind::Other));
}
w.write_i32::<LittleEndian>(values.len() as i32)?;
for value in values {
write(w, value)?;
}
Ok(())
}
fn is_ucs2(number: impl Into<u32>) -> bool {
let number = number.into();
number <= 0xd7ff || number >= 0xe000
}
fn write_string(w: &mut impl Write, s: impl AsRef<str>) -> io::Result<()> {
let s = s.as_ref();
if s.is_ascii() {
let len = s.len() + 1;
assert!(len <= i32::max_value() as usize);
w.write_i32::<LittleEndian>(len as i32)?;
w.write_all(s.as_bytes())?;
w.write_u8(0)?;
} else {
let len = -(((s.len() + 1) * 2) as isize);
assert!(len >= i32::min_value() as isize);
w.write_i32::<LittleEndian>(len as i32)?;
for character in s.chars() {
if is_ucs2(character) {
w.write_u16::<LittleEndian>(character as u16)?;
} else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"String contains non-UCS2 characters",
));
}
}
w.write_u16::<LittleEndian>(0)?;
}
Ok(())
}
fn write_uuid(w: &mut impl Write, uuid: &Uuid) -> io::Result<()> {
let mut abcd = [0; 4];
BigEndian::read_u32_into(uuid.as_bytes(), &mut abcd);
for element in abcd.iter() {
w.write_u32::<LittleEndian>(*element)?;
}
Ok(())
}
fn write_date_time(w: &mut impl Write, date_time: DateTime<Utc>) -> io::Result<()> {
let duration = date_time - ue4_date_time_base();
let duration = duration
.to_std()
.unwrap_or(std::time::Duration::from_secs(0));
let ticks_secs = i64::try_from(duration.as_secs() * 10_000_000).unwrap();
let ticks_nanos = i64::from(duration.subsec_nanos() / 100);
w.write_i64::<LittleEndian>(ticks_secs + ticks_nanos)
}
fn combine_orientation(direction: Direction, rotation: Rotation) -> u8 {
(u8::from(direction) << 2) | u8::from(rotation)
}