Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize the player's inventory in save files #431

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions common/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,25 @@ impl Material {
];
}

#[derive(Debug, Clone, Copy)]
pub struct MaterialOutOfBounds;

impl std::fmt::Display for MaterialOutOfBounds {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Integer input does not represent a valid material")
}
}

impl std::error::Error for MaterialOutOfBounds {}

impl TryFrom<u16> for Material {
type Error = ();
type Error = MaterialOutOfBounds;

fn try_from(value: u16) -> Result<Self, Self::Error> {
Material::VALUES.get(value as usize).ok_or(()).copied()
Material::VALUES
.get(value as usize)
.ok_or(MaterialOutOfBounds)
.copied()
}
}

Expand Down
4 changes: 4 additions & 0 deletions save/src/protos.proto
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ enum ComponentType {
POSITION = 0;
// UTF-8 text
NAME = 1;
// u16
MATERIAL = 2;
// List of u64
INVENTORY = 3;
}
8 changes: 8 additions & 0 deletions save/src/protos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub enum ComponentType {
Position = 0,
/// UTF-8 text
Name = 1,
/// u16
Material = 2,
/// List of u64
Inventory = 3,
}
impl ComponentType {
/// String value of the enum field names used in the ProtoBuf definition.
Expand All @@ -50,13 +54,17 @@ impl ComponentType {
match self {
Self::Position => "POSITION",
Self::Name => "NAME",
Self::Material => "MATERIAL",
Self::Inventory => "INVENTORY",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"POSITION" => Some(Self::Position),
"NAME" => Some(Self::Name),
"MATERIAL" => Some(Self::Material),
"INVENTORY" => Some(Self::Inventory),
_ => None,
}
}
Expand Down
112 changes: 95 additions & 17 deletions server/src/sim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ impl Sim {
let entity_id = EntityId::from_bits(u64::from_le_bytes(save_entity.entity));
let mut entity_builder = EntityBuilder::new();
entity_builder.add(entity_id);
entity_builder.add(node);
for (component_type, component_bytes) in save_entity.components {
self.load_component(
read,
Expand Down Expand Up @@ -210,7 +211,22 @@ impl Sim {
orientation: na::UnitQuaternion::identity(),
},
}));
entity_builder.add(Inventory { contents: vec![] });
}
ComponentType::Material => {
let material: u16 =
u16::from_le_bytes(component_bytes.try_into().map_err(|_| {
anyhow::anyhow!("Expected Material component in save file to be 2 bytes")
})?);
entity_builder.add(Material::try_from(material)?);
}
ComponentType::Inventory => {
let mut contents = vec![];
for chunk in component_bytes.chunks(8) {
contents.push(EntityId::from_bits(u64::from_le_bytes(
chunk.try_into().unwrap(),
)));
}
entity_builder.add(Inventory { contents });
}
}
Ok(())
Expand Down Expand Up @@ -263,6 +279,23 @@ impl Sim {
}) {
components.push((ComponentType::Name as u64, ch.name.as_bytes().into()));
}
if let Some(material) = entity.get::<&Material>() {
components.push((
ComponentType::Material as u64,
(*material as u16).to_le_bytes().into(),
));
}
if let Some(inventory) = entity.get::<&Inventory>() {
let mut serialized_inventory_contents = vec![];
for entity_id in &inventory.contents {
serialized_inventory_contents
.extend_from_slice(&entity_id.to_bits().to_le_bytes());
}
components.push((
ComponentType::Inventory as u64,
serialized_inventory_contents,
));
}
let mut repr = Vec::new();
postcard_helpers::serialize(
&SaveEntity {
Expand Down Expand Up @@ -344,7 +377,7 @@ impl Sim {
};
let inventory = Inventory { contents: vec![] };
let initial_input = CharacterInput::default();
Some(self.spawn((position, character, inventory, initial_input)))
Some(self.spawn((position.node, position, character, inventory, initial_input)))
}

pub fn deactivate_character(&mut self, entity: Entity) {
Expand All @@ -366,9 +399,9 @@ impl Sim {
entity_builder.add_bundle(bundle);
let entity = self.world.spawn(entity_builder.build());

if let Ok(position) = self.world.get::<&Position>(entity) {
self.graph_entities.insert(position.node, entity);
self.dirty_nodes.insert(position.node);
if let Ok(node) = self.world.get::<&NodeId>(entity) {
self.graph_entities.insert(*node, entity);
self.dirty_nodes.insert(*node);
}

if let Ok(character) = self.world.get::<&Character>(entity) {
Expand Down Expand Up @@ -399,8 +432,8 @@ impl Sim {
pub fn destroy(&mut self, entity: Entity) {
let id = *self.world.get::<&EntityId>(entity).unwrap();
self.entity_ids.remove(&id);
if let Ok(position) = self.world.get::<&Position>(entity) {
self.graph_entities.remove(position.node, entity);
if let Ok(node) = self.world.get::<&NodeId>(entity) {
self.graph_entities.remove(*node, entity);
}
if !self.world.satisfies::<&InactiveCharacter>(entity).unwrap() {
self.accumulated_changes.despawns.push(id);
Expand Down Expand Up @@ -489,12 +522,11 @@ impl Sim {
let mut pending_block_updates: Vec<(Entity, BlockUpdate)> = vec![];

// Simulate
for (entity, (position, character, input)) in self
for (entity, (node, position, character, input)) in self
.world
.query::<(&mut Position, &mut Character, &CharacterInput)>()
.query::<(&NodeId, &mut Position, &mut Character, &CharacterInput)>()
.iter()
{
let prev_node = position.node;
character_controller::run_character_step(
&self.cfg,
&self.graph,
Expand All @@ -507,19 +539,16 @@ impl Sim {
if let Some(block_update) = input.block_update.clone() {
pending_block_updates.push((entity, block_update));
}
if prev_node != position.node {
self.dirty_nodes.insert(prev_node);
self.graph_entities.remove(prev_node, entity);
self.graph_entities.insert(position.node, entity);
}
self.dirty_nodes.insert(position.node);
self.dirty_nodes.insert(*node);
}

for (entity, block_update) in pending_block_updates {
let id = *self.world.get::<&EntityId>(entity).unwrap();
self.attempt_block_update(id, block_update);
}

self.update_entity_node_ids();

let spawns = std::mem::take(&mut self.accumulated_changes).into_spawns(
self.step,
&self.world,
Expand Down Expand Up @@ -548,6 +577,51 @@ impl Sim {
(spawns, delta)
}

/// Ensure that the NodeId component of every entity is set to what it should be to ensure consistency. Any entity
/// with a position should have a NodeId that matches that position, and all entities with inventories should propagate
/// their NodeId to their inventory items.
fn update_entity_node_ids(&mut self) {
// Helper function for properly changing the NodeId of a given node without leaving any
// of the supporting structures out of date.
let mut update_node_id = |entity: Entity, node_id: &mut NodeId, new_node_id: NodeId| {
if *node_id != new_node_id {
self.dirty_nodes.insert(*node_id);
self.graph_entities.remove(*node_id, entity);

*node_id = new_node_id;
self.dirty_nodes.insert(*node_id);
self.graph_entities.insert(*node_id, entity);
}
};

// Synchronize NodeId and Position
for (entity, (node_id, position)) in self.world.query::<(&mut NodeId, &Position)>().iter() {
update_node_id(entity, node_id, position.node);
}

// Synchronize NodeId for all inventory items.
// TODO: Note that the order in which inventory items are updated is arbitrary, so
// if inventory items can themselves have inventories, their respective NodeIds
// may be out of date by a few steps, which could cause bugs. This can be solved with
// a more complete entity hierarchy system
for (_, (&inventory_node_id, inventory)) in
self.world.query::<(&NodeId, &Inventory)>().iter()
{
for inventory_entity_id in &inventory.contents {
let inventory_entity = *self.entity_ids.get(inventory_entity_id).unwrap();

let mut inventory_entity_node_id =
self.world.get::<&mut NodeId>(inventory_entity).unwrap();

update_node_id(
inventory_entity,
&mut inventory_entity_node_id,
inventory_node_id,
);
}
}
}

/// Should be called after any set of changes is made to the graph to ensure that the server
/// does not have any partially-initialized graph nodes.
fn populate_fresh_graph_nodes(&mut self) {
Expand Down Expand Up @@ -612,6 +686,10 @@ impl Sim {
/// Executes the requested block update if the subject is able to do so and
/// leaves the state of the world unchanged otherwise
fn attempt_block_update(&mut self, subject: EntityId, block_update: BlockUpdate) {
let subject_node = *self
.world
.get::<&NodeId>(*self.entity_ids.get(&subject).unwrap())
.unwrap();
let Some(old_material) = self
.graph
.get_material(block_update.chunk_id, block_update.coords)
Expand Down Expand Up @@ -644,7 +722,7 @@ impl Sim {
self.destroy(consumed_entity);
}
if old_material != Material::Void {
let (produced_entity, _) = self.spawn((old_material,));
let (produced_entity, _) = self.spawn((subject_node, old_material));
self.add_to_inventory(subject, produced_entity);
}
}
Expand Down