diff --git a/.gitignore b/.gitignore index eb1d4ef..8c18757 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ /target Cargo.lock -.vscode \ No newline at end of file +.vscode + +# Local dir names for temporary data +/data +/work +/tmp diff --git a/README.md b/README.md index 42059db..990e027 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,16 @@ Blotter file format parser and Logic World sandbox editor in Rust. This is the version that is supported for loading/saving and high-level editing: -- **Blotter v6** (Logic World 0.91 beta) - - Not formalized yet, mostly same as v5, but component positions are - integers and custom data formats changed for some components +- [**Blotter v7**][spec-v7] (Logic World 0.92) ## Legacy versions There are no high-level APIs for these versions, but converting the savefile to newer versions is supported: -- [**Blotter v5**][v5] (Logic World 0.90) +- [**Blotter v5**][spec-v5] (Logic World 0.90) +- [**Blotter v6**][spec-v6] (Logic World 0.91) -[v5]: https://gist.github.com/JimmyCushnie/bebea37a21acbb6e669589967f9512a2 +[spec-v5]: https://gist.github.com/JimmyCushnie/bebea37a21acbb6e669589967f9512a2/f3023f8ea859d1326c50ab71ac1a76cecabb3386 +[spec-v6]: https://gist.github.com/JimmyCushnie/bebea37a21acbb6e669589967f9512a2/d02f96a57d72d6116321e4884e34551f65d3d342 +[spec-v7]: https://wiki.logic.world/wiki/Blotter_File_Format/v7 diff --git a/src/convert/mod.rs b/src/convert/mod.rs deleted file mode 100644 index 795cd03..0000000 --- a/src/convert/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod v5v6; diff --git a/src/convert/v5v6.rs b/src/convert/v5v6.rs deleted file mode 100644 index 9b5c964..0000000 --- a/src/convert/v5v6.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{v5, v6}; - -impl From for v6::BlotterFile { - fn from(file: v5::BlotterFile) -> Self { - Self { - game_version: file.game_version, - save_type: file.save_type, - mods: file.mods, - component_types: file.component_types, - components: file.components.into_iter().map(Into::into).collect(), - wires: file.wires, - circuit_states: file.circuit_states, - } - } -} - -impl From for v6::Component { - fn from(component: v5::Component) -> Self { - Self { - address: component.address, - parent: component.parent, - type_id: component.type_id, - position: floating_to_fixed_position(component.position), - rotation: component.rotation, - inputs: component.inputs, - outputs: component.outputs, - custom_data: component.custom_data, - } - } -} - -fn floating_to_fixed_position([x, y, z]: [f32; 3]) -> [i32; 3] { - const CONVERSION_FACTOR: f32 = 1000.0; - [ - (x * CONVERSION_FACTOR) as i32, - (y * CONVERSION_FACTOR) as i32, - (z * CONVERSION_FACTOR) as i32, - ] -} diff --git a/src/lib.rs b/src/lib.rs index 6c712a3..1e2401d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -mod convert; pub mod custom_data; pub mod error; pub(crate) mod io; @@ -6,18 +5,20 @@ pub(crate) mod misc; pub mod sandbox; pub mod v5; pub mod v6; +pub mod v7; use std::io::{Read, Write}; use crate::error::Error; use crate::io::{read_magic, ReadFrom}; -pub use v6 as latest; +pub use v7 as latest; #[derive(Debug)] pub enum BlotterFile { V5(v5::BlotterFile), V6(v6::BlotterFile), + V7(v7::BlotterFile), } impl BlotterFile { @@ -27,6 +28,7 @@ impl BlotterFile { match save_version { v5::SAVE_VERSION => v5::BlotterFile::read_after_save_version(reader).map(Self::V5), v6::SAVE_VERSION => v6::BlotterFile::read_after_save_version(reader).map(Self::V6), + v7::SAVE_VERSION => v7::BlotterFile::read_after_save_version(reader).map(Self::V7), _ => Err(Error::IncompatibleVersion(save_version)), } } @@ -35,6 +37,7 @@ impl BlotterFile { match self { Self::V5(file) => file.write(writer), Self::V6(file) => file.write(writer), + Self::V7(file) => file.write(writer), } } @@ -42,7 +45,8 @@ impl BlotterFile { pub fn migrate(self) -> latest::BlotterFile { match self { Self::V5(file) => Self::V6(file.into()).migrate(), - Self::V6(file) => file, + Self::V6(file) => Self::V7(file.into()).migrate(), + Self::V7(file) => file, } } } diff --git a/src/sandbox/serialize.rs b/src/sandbox/serialize.rs index 6be59bc..e5cea12 100644 --- a/src/sandbox/serialize.rs +++ b/src/sandbox/serialize.rs @@ -6,7 +6,7 @@ use crate::latest as blotter; use std::collections::{HashMap, HashSet}; /// The current game version that this save is compatible with. -const GAME_VERSION: [i32; 4] = [0, 91, 0, 510]; +const GAME_VERSION: [i32; 4] = [0, 92, 4, 18]; /// Serialization context, mainly tracks ID mappings struct Serializer { diff --git a/src/v5.rs b/src/v5.rs index 850c694..ea91c31 100644 --- a/src/v5.rs +++ b/src/v5.rs @@ -9,7 +9,7 @@ pub const SAVE_FOOTER: &[u8; 16] = b"redstone sux lol"; #[derive(Debug, Clone, Copy, PartialEq)] pub enum SaveType { World, - Subassembly, + PartialWorld, } impl ReadFrom for SaveType { @@ -17,7 +17,7 @@ impl ReadFrom for SaveType { let id = u8::read_from(reader)?; match id { 1 => Ok(Self::World), - 2 => Ok(Self::Subassembly), + 2 => Ok(Self::PartialWorld), _ => Err(Error::InvalidSave), } } @@ -27,7 +27,7 @@ impl WriteTo for SaveType { fn write_to(&self, writer: &mut W) -> Result<(), Error> { let id: u8 = match self { Self::World => 1, - Self::Subassembly => 2, + Self::PartialWorld => 2, }; id.write_to(writer)?; Ok(()) @@ -291,7 +291,7 @@ impl WriteTo for Wire { #[derive(Debug)] pub enum CircuitStates { WorldFormat { circuit_states: Vec }, - SubassemblyFormat { on_states: Vec }, + PartialWorldFormat { on_states: Vec }, } impl ReadFromSeed for CircuitStates { @@ -303,10 +303,10 @@ impl ReadFromSeed for CircuitStates { reader.read_exact(&mut circuit_states)?; Ok(Self::WorldFormat { circuit_states }) } - SaveType::Subassembly => { + SaveType::PartialWorld => { let num_on_states = usize::read_from(reader)?; let on_states = Vec::read_from_seed(reader, Length(num_on_states))?; - Ok(Self::SubassemblyFormat { on_states }) + Ok(Self::PartialWorldFormat { on_states }) } } } @@ -319,7 +319,7 @@ impl WriteTo for CircuitStates { circuit_states.len().write_to(writer)?; writer.write_all(&circuit_states)?; } - Self::SubassemblyFormat { on_states } => { + Self::PartialWorldFormat { on_states } => { on_states.len().write_to(writer)?; on_states.write_to(writer)?; } @@ -416,7 +416,7 @@ impl BlotterFile { match (self.save_type, &self.circuit_states) { (SaveType::World, CircuitStates::WorldFormat { .. }) => {} - (SaveType::Subassembly, CircuitStates::SubassemblyFormat { .. }) => {} + (SaveType::PartialWorld, CircuitStates::PartialWorldFormat { .. }) => {} _ => { return Err(Error::InvalidSave); } diff --git a/src/v6.rs b/src/v6.rs index 78b148d..e61c3cf 100644 --- a/src/v6.rs +++ b/src/v6.rs @@ -1,5 +1,6 @@ use crate::error::Error; use crate::io::*; +use crate::v5; use std::io::{Read, Write}; pub const SAVE_VERSION: u8 = 6; @@ -85,6 +86,30 @@ impl WriteTo for Component { } } +impl From for Component { + fn from(component: v5::Component) -> Self { + Self { + address: component.address, + parent: component.parent, + type_id: component.type_id, + position: v5_position_to_v6(component.position), + rotation: component.rotation, + inputs: component.inputs, + outputs: component.outputs, + custom_data: component.custom_data, + } + } +} + +fn v5_position_to_v6([x, y, z]: [f32; 3]) -> [i32; 3] { + const CONVERSION_FACTOR: f32 = 1000.0; + [ + (x * CONVERSION_FACTOR) as i32, + (y * CONVERSION_FACTOR) as i32, + (z * CONVERSION_FACTOR) as i32, + ] +} + #[derive(Debug)] pub struct BlotterFile { pub game_version: [i32; 4], @@ -173,7 +198,7 @@ impl BlotterFile { match (self.save_type, &self.circuit_states) { (SaveType::World, CircuitStates::WorldFormat { .. }) => {} - (SaveType::Subassembly, CircuitStates::SubassemblyFormat { .. }) => {} + (SaveType::PartialWorld, CircuitStates::PartialWorldFormat { .. }) => {} _ => { return Err(Error::InvalidSave); } @@ -184,3 +209,17 @@ impl BlotterFile { Ok(()) } } + +impl From for BlotterFile { + fn from(file: v5::BlotterFile) -> Self { + Self { + game_version: file.game_version, + save_type: file.save_type, + mods: file.mods, + component_types: file.component_types, + components: file.components.into_iter().map(Into::into).collect(), + wires: file.wires, + circuit_states: file.circuit_states, + } + } +} diff --git a/src/v7.rs b/src/v7.rs new file mode 100644 index 0000000..56494fa --- /dev/null +++ b/src/v7.rs @@ -0,0 +1,247 @@ +use crate::error::Error; +use crate::io::*; +use crate::v6; +use std::io::{Read, Write}; + +pub const SAVE_VERSION: u8 = 7; + +// Unchanged from previous version: +pub use crate::v6::{ + CircuitStates, Component, ComponentType, Input, ModInfo, Output, SaveType, SAVE_FOOTER, + SAVE_HEADER, +}; + +#[derive(Debug, Clone, Copy)] +pub enum PegType { + Input, + Output, +} + +impl ReadFrom for PegType { + fn read_from(reader: &mut R) -> Result { + let id = u8::read_from(reader)?; + match id { + 1 => Ok(Self::Input), + 2 => Ok(Self::Output), + _ => Err(Error::InvalidSave), + } + } +} + +impl WriteTo for PegType { + fn write_to(&self, writer: &mut W) -> Result<(), Error> { + let id: u8 = match self { + Self::Input => 1, + Self::Output => 2, + }; + id.write_to(writer)?; + Ok(()) + } +} + +impl From for PegType { + fn from(x: v6::PegType) -> Self { + match x { + v6::PegType::Input => Self::Input, + v6::PegType::Output => Self::Output, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PegAddress { + pub peg_type: PegType, + pub component_address: u32, + pub peg_index: i32, +} + +impl ReadFrom for PegAddress { + fn read_from(reader: &mut R) -> Result { + let peg_type = ReadFrom::read_from(reader)?; + let component_address = ReadFrom::read_from(reader)?; + let peg_index = ReadFrom::read_from(reader)?; + + Ok(Self { + peg_type, + component_address, + peg_index, + }) + } +} + +impl WriteTo for PegAddress { + fn write_to(&self, writer: &mut W) -> Result<(), Error> { + self.peg_type.write_to(writer)?; + self.component_address.write_to(writer)?; + self.peg_index.write_to(writer)?; + Ok(()) + } +} + +impl From for PegAddress { + fn from(x: v6::PegAddress) -> Self { + Self { + peg_type: x.peg_type.into(), + component_address: x.component_address, + peg_index: x.peg_index, + } + } +} + +#[derive(Debug)] +pub struct Wire { + pub start_peg: PegAddress, + pub end_peg: PegAddress, + pub circuit_state_id: i32, + pub rotation: f32, +} + +impl ReadFrom for Wire { + fn read_from(reader: &mut R) -> Result { + let start_peg = ReadFrom::read_from(reader)?; + let end_peg = ReadFrom::read_from(reader)?; + let circuit_state_id = ReadFrom::read_from(reader)?; + let rotation = ReadFrom::read_from(reader)?; + Ok(Self { + start_peg, + end_peg, + circuit_state_id, + rotation, + }) + } +} + +impl WriteTo for Wire { + fn write_to(&self, writer: &mut W) -> Result<(), Error> { + self.start_peg.write_to(writer)?; + self.end_peg.write_to(writer)?; + self.circuit_state_id.write_to(writer)?; + self.rotation.write_to(writer)?; + Ok(()) + } +} + +impl From for Wire { + fn from(x: v6::Wire) -> Self { + Self { + start_peg: x.start_peg.into(), + end_peg: x.end_peg.into(), + circuit_state_id: x.circuit_state_id, + rotation: x.rotation, + } + } +} + +#[derive(Debug)] +pub struct BlotterFile { + pub game_version: [i32; 4], + pub save_type: SaveType, + pub mods: Vec, + pub component_types: Vec, + pub components: Vec, + pub wires: Vec, + pub circuit_states: CircuitStates, +} + +impl BlotterFile { + pub fn new(game_version: [i32; 4]) -> Self { + Self { + game_version, + save_type: SaveType::World, + mods: Vec::new(), + component_types: Vec::new(), + components: Vec::new(), + wires: Vec::new(), + circuit_states: CircuitStates::WorldFormat { + circuit_states: Vec::new(), + }, + } + } + + pub fn read(reader: &mut R) -> Result { + read_magic(reader, SAVE_HEADER)?; + + let save_version = u8::read_from(reader)?; + if save_version != SAVE_VERSION { + return Err(Error::IncompatibleVersion(save_version)); + } + Self::read_after_save_version(reader) + } + + pub(crate) fn read_after_save_version(reader: &mut R) -> Result { + let game_version = ReadFrom::read_from(reader)?; + + let save_type = SaveType::read_from(reader)?; + let num_components = usize::read_from(reader)?; + let num_wires = usize::read_from(reader)?; + + let num_mods = usize::read_from(reader)?; + let mods = Vec::read_from_seed(reader, Length(num_mods))?; + + let num_component_types: usize = usize::read_from(reader)?; + let component_types = Vec::read_from_seed(reader, Length(num_component_types))?; + + let components = Vec::read_from_seed(reader, Length(num_components))?; + let wires = Vec::read_from_seed(reader, Length(num_wires))?; + + let circuit_states = CircuitStates::read_from_seed(reader, save_type)?; + + read_magic(reader, SAVE_FOOTER)?; + + Ok(Self { + game_version, + save_type, + mods, + component_types, + components, + wires, + circuit_states, + }) + } + + pub fn write(&self, writer: &mut W) -> Result<(), Error> { + writer.write_all(SAVE_HEADER)?; + + SAVE_VERSION.write_to(writer)?; + self.game_version.write_to(writer)?; + self.save_type.write_to(writer)?; + + self.components.len().write_to(writer)?; + self.wires.len().write_to(writer)?; + + self.mods.len().write_to(writer)?; + self.mods.write_to(writer)?; + + self.component_types.len().write_to(writer)?; + self.component_types.write_to(writer)?; + + self.components.write_to(writer)?; + self.wires.write_to(writer)?; + + match (self.save_type, &self.circuit_states) { + (SaveType::World, CircuitStates::WorldFormat { .. }) => {} + (SaveType::PartialWorld, CircuitStates::PartialWorldFormat { .. }) => {} + _ => { + return Err(Error::InvalidSave); + } + } + self.circuit_states.write_to(writer)?; + + writer.write_all(SAVE_FOOTER)?; + Ok(()) + } +} + +impl From for BlotterFile { + fn from(x: v6::BlotterFile) -> Self { + Self { + game_version: x.game_version, + save_type: x.save_type, + mods: x.mods, + component_types: x.component_types, + components: x.components, + wires: x.wires.into_iter().map(Into::into).collect(), + circuit_states: x.circuit_states, + } + } +}