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

Save and load all modified chunks #331

Merged
merged 2 commits into from
Jan 11, 2024
Merged
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
2 changes: 1 addition & 1 deletion client/src/graphics/voxels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl Voxels {

// Now that the block is populated, we can apply any pending block updates the server
// provided that the client couldn't apply.
if let Some(block_updates) = sim.pending_modified_chunks.remove(&chunk_id) {
if let Some(block_updates) = sim.preloaded_block_updates.remove(&chunk_id) {
for block_update in block_updates {
// The chunk was just populated, so a block update should always succeed.
assert!(sim.graph.update_block(&block_update));
Expand Down
12 changes: 6 additions & 6 deletions client/src/sim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const MATERIAL_PALETTE: [Material; 10] = [
pub struct Sim {
// World state
pub graph: Graph,
pub pending_modified_chunks: FxHashMap<ChunkId, Vec<BlockUpdate>>,
/// Voxel data that have been downloaded from the server for chunks not yet introduced to the graph
pub preloaded_block_updates: FxHashMap<ChunkId, Vec<BlockUpdate>>,
pub graph_entities: GraphEntities,
entity_ids: FxHashMap<EntityId, Entity>,
pub world: hecs::World,
Expand Down Expand Up @@ -84,7 +85,7 @@ impl Sim {
populate_fresh_nodes(&mut graph);
Self {
graph,
pending_modified_chunks: FxHashMap::default(),
preloaded_block_updates: FxHashMap::default(),
graph_entities: GraphEntities::new(),
entity_ids: FxHashMap::default(),
world: hecs::World::new(),
Expand Down Expand Up @@ -324,15 +325,14 @@ impl Sim {
populate_fresh_nodes(&mut self.graph);
for block_update in msg.block_updates.into_iter() {
if !self.graph.update_block(&block_update) {
self.pending_modified_chunks
self.preloaded_block_updates
.entry(block_update.chunk_id)
.or_default()
.push(block_update);
}
}
for (chunk_id, voxel_data) in msg.modified_chunks {
let Some(voxel_data) = VoxelData::from_serializable(&voxel_data, self.cfg.chunk_size)
else {
for (chunk_id, voxel_data) in msg.voxel_data {
let Some(voxel_data) = VoxelData::deserialize(&voxel_data, self.cfg.chunk_size) else {
tracing::error!("Voxel data received from server is of incorrect dimension");
continue;
};
Expand Down
5 changes: 5 additions & 0 deletions common/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ impl Graph {
node.0
}

#[inline]
pub fn from_hash(&self, hash: u128) -> NodeId {
NodeId(hash)
}

/// Ensure all shorter neighbors of a not-yet-created child node exist and return them, excluding the given parent node
fn populate_shorter_neighbors_of_child(
&mut self,
Expand Down
37 changes: 20 additions & 17 deletions common/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::collision_math::Ray;
use crate::dodeca::Vertex;
use crate::graph::{Graph, NodeId};
use crate::lru_slab::SlotId;
use crate::proto::{BlockUpdate, Position, SerializableVoxelData};
use crate::proto::{BlockUpdate, Position, SerializedVoxelData};
use crate::world::Material;
use crate::worldgen::NodeState;
use crate::{math, Chunks};
Expand Down Expand Up @@ -326,46 +326,49 @@ impl VoxelData {
}
}

/// Returns a `VoxelData` with void margins based on the given `SerializableVoxelData`, or `None` if
/// the `SerializableVoxelData` came from a `VoxelData` with the wrong dimension.
pub fn from_serializable(serializable: &SerializableVoxelData, dimension: u8) -> Option<Self> {
if serializable.voxels.len() != usize::from(dimension).pow(3) {
/// Returns a `VoxelData` with void margins based on the given `SerializedVoxelData`, or `None` if
/// the `SerializedVoxelData` came from a `VoxelData` with the wrong dimension or an unknown material.
pub fn deserialize(serialized: &SerializedVoxelData, dimension: u8) -> Option<Self> {
if serialized.inner.len() != usize::from(dimension).pow(3) * 2 {
return None;
}

let mut materials = serialized
.inner
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]));

let mut data = vec![Material::Void; (usize::from(dimension) + 2).pow(3)];
let mut input_index = 0;
for z in 0..dimension {
for y in 0..dimension {
for x in 0..dimension {
// We cannot use a linear copy here because `data` has margins, while `serializable.voxels` does not.
data[Coords([x, y, z]).to_index(dimension)] = serializable.voxels[input_index];
input_index += 1;
// We cannot use a linear copy here because `data` has margins, while `serialized.inner` does not.
data[Coords([x, y, z]).to_index(dimension)] =
materials.next().unwrap().try_into().ok()?;
}
}
}
Some(VoxelData::Dense(data.into_boxed_slice()))
}

/// Returns a `SerializableVoxelData` corresponding to `self`. Assumes that`self` is `Dense` and
/// Returns a `SerializedVoxelData` corresponding to `self`. Assumes that `self` is `Dense` and
/// has the right dimension, as it will panic or return incorrect data otherwise.
pub fn to_serializable(&self, dimension: u8) -> SerializableVoxelData {
pub fn serialize(&self, dimension: u8) -> SerializedVoxelData {
let VoxelData::Dense(data) = self else {
panic!("Only dense chunks can be serialized.");
};

let mut serializable: Vec<Material> = Vec::with_capacity(usize::from(dimension).pow(3));
let mut serialized: Vec<u8> = Vec::with_capacity(usize::from(dimension).pow(3) * 2);
for z in 0..dimension {
for y in 0..dimension {
for x in 0..dimension {
// We cannot use a linear copy here because `data` has margins, while `serializable.voxels` does not.
serializable.push(data[Coords([x, y, z]).to_index(dimension)]);
// We cannot use a linear copy here because `data` has margins, while `serialized.inner` does not.
serialized
.extend((data[Coords([x, y, z]).to_index(dimension)] as u16).to_le_bytes());
}
}
}
SerializableVoxelData {
voxels: serializable,
}
SerializedVoxelData { inner: serialized }
}
}

Expand Down
7 changes: 4 additions & 3 deletions common/src/proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub struct Spawns {
pub despawns: Vec<EntityId>,
pub nodes: Vec<FreshNode>,
pub block_updates: Vec<BlockUpdate>,
pub modified_chunks: Vec<(ChunkId, SerializableVoxelData)>,
pub voxel_data: Vec<(ChunkId, SerializedVoxelData)>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -84,8 +84,9 @@ pub struct BlockUpdate {
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SerializableVoxelData {
pub voxels: Vec<Material>,
pub struct SerializedVoxelData {
/// Dense 3D array of 16-bit material tags for all voxels in this chunk
pub inner: Vec<u8>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down
65 changes: 65 additions & 0 deletions common/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,68 @@ pub enum Material {
impl Material {
pub const COUNT: usize = 40;
}

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

fn try_from(value: u16) -> Result<Self, Self::Error> {
Ok(match value {
0 => Material::Void,
1 => Material::Dirt,
2 => Material::Sand,
3 => Material::Silt,
4 => Material::Clay,
5 => Material::Mud,
6 => Material::SandyLoam,
7 => Material::SiltyLoam,
8 => Material::ClayLoam,
9 => Material::RedSand,
10 => Material::Limestone,
11 => Material::Shale,
12 => Material::Dolomite,
13 => Material::Sandstone,
14 => Material::RedSandstone,
15 => Material::Marble,
16 => Material::Slate,
17 => Material::Granite,
18 => Material::Diorite,
19 => Material::Andesite,
20 => Material::Gabbro,
21 => Material::Basalt,
22 => Material::Olivine,
23 => Material::Water,
24 => Material::Lava,
25 => Material::Wood,
26 => Material::Leaves,
27 => Material::WoodPlanks,
28 => Material::GreyBrick,
29 => Material::WhiteBrick,
30 => Material::Ice,
31 => Material::IceSlush,
32 => Material::Gravel,
33 => Material::Snow,
34 => Material::CoarseGrass,
35 => Material::TanGrass,
36 => Material::LushGrass,
37 => Material::MudGrass,
38 => Material::Grass,
39 => Material::CaveGrass,
_ => Err(())?,
})
}
}

#[cfg(test)]
mod tests {
use super::Material;

#[test]
fn u16_to_material_consistency_check() {
for i in 0..Material::COUNT {
let index = u16::try_from(i).unwrap();
let material =
Material::try_from(index).expect("no missing entries in try_from match statement");
assert_eq!(index, material as u16);
}
}
}
10 changes: 10 additions & 0 deletions save/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ impl Reader<'_> {
.map_err(GetError::DecompressionFailed)?;
Ok(Some(Character::decode(&*self.accum)?))
}

/// Temporary function to load all voxel-related save data at once.
/// TODO: Replace this implementation with a streaming implementation
/// that does not require loading everything at once
pub fn get_all_voxel_node_ids(&self) -> Result<Vec<u128>, GetError> {
self.voxel_nodes
.iter()?
.map(|n| Ok(n.map_err(GetError::from)?.0.value()))
.collect()
}
}

fn decompress(
Expand Down
4 changes: 2 additions & 2 deletions server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl Server {
fn new(params: SimConfig, save: Save) -> Self {
let cfg = Arc::new(params);
Self {
sim: Sim::new(cfg.clone()),
sim: Sim::new(cfg.clone(), &save),
cfg,
clients: DenseSlotMap::default(),
save,
Expand Down Expand Up @@ -125,7 +125,7 @@ impl Server {
|| !spawns.despawns.is_empty()
|| !spawns.nodes.is_empty()
|| !spawns.block_updates.is_empty()
|| !spawns.modified_chunks.is_empty()
|| !spawns.voxel_data.is_empty()
{
handles.ordered.try_send(spawns.clone())
} else {
Expand Down
Loading
Loading