Sandbox: Add savefile loading
This commit is contained in:
parent
84a7cdccc2
commit
d9f6311ae1
3 changed files with 177 additions and 20 deletions
|
|
@ -26,17 +26,22 @@ pub struct Sandbox {
|
||||||
impl Sandbox {
|
impl Sandbox {
|
||||||
/// Create a new, empty sandbox level.
|
/// Create a new, empty sandbox level.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
Self::with_meta_info(default_component_types_map(), Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an empty sandbox level with custom component-type / mod info.
|
||||||
|
fn with_meta_info(component_types: HashMap<String, u16>, mods: Vec<ModInfo>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
next_component: 1,
|
next_component: 1,
|
||||||
next_cluster: 0,
|
next_cluster: 0,
|
||||||
next_wire: 0,
|
next_wire: 0,
|
||||||
next_type: 0,
|
next_type: component_types.values().copied().max().unwrap_or(0),
|
||||||
root_components: HashSet::new(),
|
root_components: HashSet::new(),
|
||||||
components: HashMap::new(),
|
components: HashMap::new(),
|
||||||
wires: HashMap::new(),
|
wires: HashMap::new(),
|
||||||
clusters: HashMap::new(),
|
clusters: HashMap::new(),
|
||||||
component_types: default_component_types_map(),
|
component_types,
|
||||||
mods: Vec::new(),
|
mods,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,46 +66,95 @@ impl Sandbox {
|
||||||
.collect(),
|
.collect(),
|
||||||
custom_data: component.custom_data.clone(),
|
custom_data: component.custom_data.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = ComponentId(NonZeroU32::new(self.next_component).unwrap());
|
let id = ComponentId(NonZeroU32::new(self.next_component).unwrap());
|
||||||
self.next_component += 1;
|
self.insert_component(id, info);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
self.components.insert(id, info);
|
/// Internal logic of `add_component` that is shared with savefile loading.
|
||||||
if let Some(parent) = component.parent {
|
/// Savefiles store the component and cluster IDs, so they can be directly
|
||||||
|
/// passed instead of being allocated (as is done by `add_component`).
|
||||||
|
fn insert_component(&mut self, id: ComponentId, info: ComponentInfo) {
|
||||||
|
// TODO bubble this error (could be triggered by an invalid savefile).
|
||||||
|
assert!(!self.components.contains_key(&id));
|
||||||
|
|
||||||
|
// Update component ID allocator.
|
||||||
|
// TODO make this less sparse, 2 billion is not very much to work with
|
||||||
|
// over the lifetime of a world.
|
||||||
|
self.next_component = self.next_component.max(id.into_raw() + 1);
|
||||||
|
|
||||||
|
// Add parent-child cross-reference.
|
||||||
|
if let Some(parent) = info.parent {
|
||||||
|
// Valid savefiles will store and load the parent before the child,
|
||||||
|
// so it is reasonable to assume the parent exists here.
|
||||||
|
//TODO bubble this error; it should be recoverable in file loading.
|
||||||
self.components
|
self.components
|
||||||
.get_mut(&parent)
|
.get_mut(&parent)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.children
|
.children
|
||||||
.insert(id);
|
.insert(id);
|
||||||
|
} else {
|
||||||
|
self.root_components.insert(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
id
|
// Add component info.
|
||||||
|
self.components.insert(id, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_wire(
|
pub fn add_wire(
|
||||||
&mut self,
|
&mut self,
|
||||||
addr_a: PegAddress,
|
addr_a: PegAddress,
|
||||||
addr_b: PegAddress,
|
addr_b: PegAddress,
|
||||||
|
rotation: f32,
|
||||||
|
) -> Result<WireId, AddWireError> {
|
||||||
|
self.insert_wire(addr_a, addr_b, rotation, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like the `add_component` / `insert_component` duality, this has logic
|
||||||
|
/// shared between `add_wire` and savefile loading. Unlike
|
||||||
|
/// `insert_component`, this does allocate the wire ID automatically,
|
||||||
|
/// because the save file doesn't use wire IDs.
|
||||||
|
fn insert_wire(
|
||||||
|
&mut self,
|
||||||
|
addr_a: PegAddress,
|
||||||
|
addr_b: PegAddress,
|
||||||
|
rotation: f32,
|
||||||
|
cluster_id: Option<ClusterId>,
|
||||||
) -> Result<WireId, AddWireError> {
|
) -> Result<WireId, AddWireError> {
|
||||||
// It is illegal to directly connect output pegs.
|
// It is illegal to directly connect output pegs.
|
||||||
if addr_a.peg_type == PegType::Output && addr_b.peg_type == PegType::Output {
|
if addr_a.peg_type == PegType::Output && addr_b.peg_type == PegType::Output {
|
||||||
return Err(AddWireError::InvalidPegAddress)?;
|
return Err(AddWireError::InvalidPegAddress)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is already a wire connecting these pegs, nothing needs to be
|
|
||||||
// done.
|
|
||||||
let peg_a = self
|
let peg_a = self
|
||||||
.get_peg(&addr_a)
|
.get_peg(&addr_a)
|
||||||
.ok_or(AddWireError::InvalidPegAddress)?;
|
.ok_or(AddWireError::InvalidPegAddress)?;
|
||||||
let peg_b = self
|
let peg_b = self
|
||||||
.get_peg(&addr_b)
|
.get_peg(&addr_b)
|
||||||
.ok_or(AddWireError::InvalidPegAddress)?;
|
.ok_or(AddWireError::InvalidPegAddress)?;
|
||||||
|
// If there is already a wire connecting these pegs, nothing needs to be
|
||||||
|
// done.
|
||||||
if let Some(&wire_id) = peg_a.wires.intersection(&peg_b.wires).next() {
|
if let Some(&wire_id) = peg_a.wires.intersection(&peg_b.wires).next() {
|
||||||
return Ok(wire_id);
|
return Ok(wire_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure both pegs have the same cluster.
|
let cluster_id = match cluster_id {
|
||||||
let cluster_id = self.merge_clusters(peg_a.cluster_id, peg_b.cluster_id);
|
Some(id) => {
|
||||||
|
// If cluster ID is specified (i.e. from an existing savefile),
|
||||||
|
// check savefile validity: ensure that both endpoints and the
|
||||||
|
// wire all have the same cluster.
|
||||||
|
|
||||||
|
//TODO bubble this error
|
||||||
|
assert!(id == peg_a.cluster_id && id == peg_b.cluster_id);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// If no cluster ID is given (i.e. caller is `add_wire`), then
|
||||||
|
// obtain one from merging the two endpoints.
|
||||||
|
self.merge_clusters(peg_a.cluster_id, peg_b.cluster_id)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Create wire.
|
// Create wire.
|
||||||
let wire_id = WireId(self.next_wire);
|
let wire_id = WireId(self.next_wire);
|
||||||
|
|
@ -111,7 +165,7 @@ impl Sandbox {
|
||||||
a: addr_a,
|
a: addr_a,
|
||||||
b: addr_b,
|
b: addr_b,
|
||||||
cluster_id,
|
cluster_id,
|
||||||
rotation: 0.0,
|
rotation,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -265,38 +319,47 @@ impl<'a> ComponentBuilder<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ComponentId(NonZeroU32);
|
pub struct ComponentId(NonZeroU32);
|
||||||
|
|
||||||
impl ComponentId {
|
impl ComponentId {
|
||||||
fn into_raw(self) -> u32 {
|
fn into_raw(self) -> u32 {
|
||||||
self.0.into()
|
self.0.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_raw(raw: u32) -> Option<Self> {
|
||||||
|
NonZeroU32::new(raw).map(Self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ClusterId(u32);
|
pub struct ClusterId(u32);
|
||||||
|
|
||||||
impl ClusterId {
|
impl ClusterId {
|
||||||
fn into_raw(self) -> i32 {
|
fn into_raw(self) -> i32 {
|
||||||
self.0.try_into().unwrap()
|
self.0.try_into().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_raw(raw: i32) -> Self {
|
||||||
|
Self(raw.try_into().unwrap())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct WireId(u32);
|
pub struct WireId(u32);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum AddWireError {
|
pub enum AddWireError {
|
||||||
InvalidPegAddress,
|
InvalidPegAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum PegType {
|
pub enum PegType {
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct PegAddress {
|
pub struct PegAddress {
|
||||||
pub component: ComponentId,
|
pub component: ComponentId,
|
||||||
pub peg_type: PegType,
|
pub peg_type: PegType,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Conversions between the Sandbox editor and Blotter serialization types.
|
//! Conversions between the Sandbox editor and Blotter serialization types.
|
||||||
|
|
||||||
use crate::latest as blotter;
|
use crate::latest as blotter;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// The current game version that this save is compatible with.
|
/// 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, 91, 0, 510];
|
||||||
|
|
@ -43,6 +44,40 @@ impl From<&super::Sandbox> for blotter::BlotterFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&blotter::BlotterFile> for super::Sandbox {
|
||||||
|
fn from(file: &blotter::BlotterFile) -> Self {
|
||||||
|
// Instead of building the sandbox and all the internal cross-references
|
||||||
|
// from scratch, re-use the sandbox API as much as possible when loading
|
||||||
|
// so there is just one implementation of the cross-referencing.
|
||||||
|
let mut sandbox = super::Sandbox::with_meta_info(
|
||||||
|
file.component_types
|
||||||
|
.iter()
|
||||||
|
.map(|ctype| (ctype.text_id.clone(), ctype.numeric_id))
|
||||||
|
.collect(),
|
||||||
|
file.mods.clone(),
|
||||||
|
);
|
||||||
|
for component in &file.components {
|
||||||
|
let (id, info) = component.into();
|
||||||
|
sandbox.insert_component(id, info);
|
||||||
|
}
|
||||||
|
for wire in &file.wires {
|
||||||
|
// TODO bubble error
|
||||||
|
sandbox
|
||||||
|
.insert_wire(
|
||||||
|
wire.start_peg.into(),
|
||||||
|
wire.end_peg.into(),
|
||||||
|
wire.rotation,
|
||||||
|
Some(super::ClusterId::from_raw(wire.circuit_state_id)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO load circuit states
|
||||||
|
|
||||||
|
sandbox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<(super::ComponentId, &super::ComponentInfo)> for blotter::Component {
|
impl From<(super::ComponentId, &super::ComponentInfo)> for blotter::Component {
|
||||||
fn from((id, component): (super::ComponentId, &super::ComponentInfo)) -> Self {
|
fn from((id, component): (super::ComponentId, &super::ComponentInfo)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -58,6 +93,27 @@ impl From<(super::ComponentId, &super::ComponentInfo)> for blotter::Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&blotter::Component> for (super::ComponentId, super::ComponentInfo) {
|
||||||
|
fn from(component: &blotter::Component) -> Self {
|
||||||
|
let id = super::ComponentId::from_raw(component.address).unwrap();
|
||||||
|
let info = super::ComponentInfo {
|
||||||
|
type_id: component.type_id,
|
||||||
|
parent: super::ComponentId::from_raw(component.parent),
|
||||||
|
position: component.position,
|
||||||
|
rotation: component.rotation,
|
||||||
|
children: HashSet::new(),
|
||||||
|
inputs: component.inputs.iter().map(|input| input.into()).collect(),
|
||||||
|
outputs: component
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.map(|output| output.into())
|
||||||
|
.collect(),
|
||||||
|
custom_data: component.custom_data.clone(),
|
||||||
|
};
|
||||||
|
(id, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&super::PegInfo> for blotter::Input {
|
impl From<&super::PegInfo> for blotter::Input {
|
||||||
fn from(peg: &super::PegInfo) -> Self {
|
fn from(peg: &super::PegInfo) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -66,6 +122,15 @@ impl From<&super::PegInfo> for blotter::Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&blotter::Input> for super::PegInfo {
|
||||||
|
fn from(input: &blotter::Input) -> Self {
|
||||||
|
Self {
|
||||||
|
cluster_id: super::ClusterId::from_raw(input.circuit_state_id),
|
||||||
|
wires: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&super::PegInfo> for blotter::Output {
|
impl From<&super::PegInfo> for blotter::Output {
|
||||||
fn from(peg: &super::PegInfo) -> Self {
|
fn from(peg: &super::PegInfo) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -74,6 +139,15 @@ impl From<&super::PegInfo> for blotter::Output {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&blotter::Output> for super::PegInfo {
|
||||||
|
fn from(input: &blotter::Output) -> Self {
|
||||||
|
Self {
|
||||||
|
cluster_id: super::ClusterId::from_raw(input.circuit_state_id),
|
||||||
|
wires: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&super::WireInfo> for blotter::Wire {
|
impl From<&super::WireInfo> for blotter::Wire {
|
||||||
fn from(wire: &super::WireInfo) -> Self {
|
fn from(wire: &super::WireInfo) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -95,6 +169,17 @@ impl From<super::PegAddress> for blotter::PegAddress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<blotter::PegAddress> for super::PegAddress {
|
||||||
|
fn from(addr: blotter::PegAddress) -> Self {
|
||||||
|
//TODO bubble errors
|
||||||
|
Self {
|
||||||
|
component: super::ComponentId::from_raw(addr.component_address).unwrap(),
|
||||||
|
peg_type: addr.peg_type.into(),
|
||||||
|
peg_index: addr.peg_index.try_into().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<super::PegType> for blotter::PegType {
|
impl From<super::PegType> for blotter::PegType {
|
||||||
fn from(peg_type: super::PegType) -> Self {
|
fn from(peg_type: super::PegType) -> Self {
|
||||||
match peg_type {
|
match peg_type {
|
||||||
|
|
@ -103,3 +188,12 @@ impl From<super::PegType> for blotter::PegType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<blotter::PegType> for super::PegType {
|
||||||
|
fn from(peg_type: blotter::PegType) -> Self {
|
||||||
|
match peg_type {
|
||||||
|
blotter::PegType::Input => Self::Input,
|
||||||
|
blotter::PegType::Output => Self::Output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ impl WriteTo for Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum PegType {
|
pub enum PegType {
|
||||||
Output,
|
Output,
|
||||||
Input,
|
Input,
|
||||||
|
|
@ -225,7 +225,7 @@ impl WriteTo for PegType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct PegAddress {
|
pub struct PegAddress {
|
||||||
pub peg_type: PegType,
|
pub peg_type: PegType,
|
||||||
pub component_address: u32,
|
pub component_address: u32,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue