Skip to content

Commit

Permalink
Serialize the player's inventory in save files
Browse files Browse the repository at this point in the history
  • Loading branch information
patowen committed Jan 2, 2025
1 parent 344cf01 commit 019706e
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 19 deletions.
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

0 comments on commit 019706e

Please sign in to comment.