mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added experimental terrain persistence
This commit is contained in:
parent
0dfc0d39dd
commit
738e59965f
@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Custom error message when a supported graphics backend can not be found
|
||||
- Add server setting with PvE/PvP switch
|
||||
- Can now tilt glider while only wielding it
|
||||
- Experimental terrain persistence (see server documentation)
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -452,8 +452,13 @@ impl ChatCommand {
|
||||
Some(Admin),
|
||||
),
|
||||
ChatCommand::MakeBlock => cmd(
|
||||
vec![Enum("block", BLOCK_KINDS.clone(), Required)],
|
||||
"Make a block at your location",
|
||||
vec![
|
||||
Enum("block", BLOCK_KINDS.clone(), Required),
|
||||
Integer("r", 255, Optional),
|
||||
Integer("g", 255, Optional),
|
||||
Integer("b", 255, Optional),
|
||||
],
|
||||
"Make a block at your location with a color",
|
||||
Some(Admin),
|
||||
),
|
||||
ChatCommand::MakeSprite => cmd(
|
||||
|
@ -309,15 +309,46 @@ impl Block {
|
||||
Block::air(SpriteKind::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to convert a [`u32`] to a block
|
||||
#[inline]
|
||||
pub fn from_u32(x: u32) -> Option<Self> {
|
||||
let [bk, r, g, b] = x.to_le_bytes();
|
||||
Some(Self {
|
||||
kind: BlockKind::from_u8(bk)?,
|
||||
attr: [r, g, b],
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_u32(&self) -> u32 {
|
||||
u32::from_le_bytes([self.kind as u8, self.attr[0], self.attr[1], self.attr[2]])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn block_size() {
|
||||
assert_eq!(std::mem::size_of::<BlockKind>(), 1);
|
||||
assert_eq!(std::mem::size_of::<Block>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_u32() {
|
||||
for bk in BlockKind::iter() {
|
||||
let block = Block::new(bk, Rgb::new(165, 90, 204)); // Pretty unique bit patterns
|
||||
if bk.is_filled() {
|
||||
assert_eq!(Block::from_u32(block.to_u32()), Some(block));
|
||||
} else {
|
||||
assert_eq!(
|
||||
Block::from_u32(block.to_u32()),
|
||||
Some(Block::new(bk, Rgb::zero())),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ network = { package = "veloren-network", path = "../network", features = ["metri
|
||||
specs = { git = "https://github.com/amethyst/specs.git", features = ["shred-derive"], rev = "f985bec5d456f7b0dd8aae99848f9473c2cd9d46" }
|
||||
specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", rev = "8be2abcddf8f524cb5876e8dd20a7e47cfaf7573" }
|
||||
|
||||
bincode = "1.3.2"
|
||||
num_cpus = "1.0"
|
||||
tracing = "0.1"
|
||||
vek = { version = "0.14.1", features = ["serde"] }
|
||||
|
@ -523,13 +523,20 @@ fn handle_make_block(
|
||||
args: Vec<String>,
|
||||
action: &ChatCommand,
|
||||
) -> CmdResult<()> {
|
||||
if let Some(block_name) = parse_args!(args, String) {
|
||||
if let (Some(block_name), r, g, b) = parse_args!(args, String, u8, u8, u8) {
|
||||
if let Ok(bk) = BlockKind::from_str(block_name.as_str()) {
|
||||
let pos = position(server, target, "target")?;
|
||||
server.state.set_block(
|
||||
pos.0.map(|e| e.floor() as i32),
|
||||
Block::new(bk, Rgb::broadcast(255)),
|
||||
);
|
||||
let new_block = Block::new(bk, Rgb::new(r, g, b).map(|e| e.unwrap_or(255)));
|
||||
let pos = pos.0.map(|e| e.floor() as i32);
|
||||
server.state.set_block(pos, new_block);
|
||||
if let Some(terrain_persistence) = server
|
||||
.state
|
||||
.ecs()
|
||||
.try_fetch_mut::<crate::TerrainPersistence>()
|
||||
.as_mut()
|
||||
{
|
||||
terrain_persistence.set_block(pos, new_block);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Invalid block kind: {}", block_name))
|
||||
@ -557,6 +564,14 @@ fn handle_make_sprite(
|
||||
.unwrap_or_else(|| Block::air(SpriteKind::Empty))
|
||||
.with_sprite(sk);
|
||||
server.state.set_block(pos, new_block);
|
||||
if let Some(terrain_persistence) = server
|
||||
.state
|
||||
.ecs()
|
||||
.try_fetch_mut::<crate::TerrainPersistence>()
|
||||
.as_mut()
|
||||
{
|
||||
terrain_persistence.set_block(pos, new_block);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Invalid sprite kind: {}", sprite_name))
|
||||
@ -1392,12 +1407,23 @@ fn handle_build(
|
||||
.write_storage::<comp::CanBuild>()
|
||||
.get_mut(target)
|
||||
{
|
||||
let toggle_string = if can_build.enabled { "off" } else { "on" };
|
||||
let chat_msg = ServerGeneral::server_msg(
|
||||
ChatType::CommandInfo,
|
||||
format!("Toggled {:?} build mode!", toggle_string),
|
||||
);
|
||||
can_build.enabled ^= true;
|
||||
|
||||
let toggle_string = if can_build.enabled { "on" } else { "off" };
|
||||
let msg = format!(
|
||||
"Toggled build mode {}.{}",
|
||||
toggle_string,
|
||||
if !can_build.enabled {
|
||||
""
|
||||
} else if server.settings().experimental_terrain_persistence {
|
||||
" Experimental terrain persistence is enabled. The server will attempt to persist \
|
||||
changes, but this is not guaranteed."
|
||||
} else {
|
||||
" Changes will not be persisted when a chunk unloads."
|
||||
},
|
||||
);
|
||||
|
||||
let chat_msg = ServerGeneral::server_msg(ChatType::CommandInfo, msg);
|
||||
if client != target {
|
||||
server.notify_client(target, chat_msg.clone());
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ pub mod rtsim;
|
||||
pub mod settings;
|
||||
pub mod state_ext;
|
||||
pub mod sys;
|
||||
pub mod terrain_persistence;
|
||||
#[cfg(not(feature = "worldgen"))] mod test_world;
|
||||
pub mod wiring;
|
||||
|
||||
@ -55,6 +56,7 @@ use crate::{
|
||||
rtsim::RtSim,
|
||||
state_ext::StateExt,
|
||||
sys::sentinel::{DeletedEntities, TrackedComps},
|
||||
terrain_persistence::TerrainPersistence,
|
||||
};
|
||||
#[cfg(not(feature = "worldgen"))]
|
||||
use common::grid::Grid;
|
||||
@ -216,6 +218,17 @@ impl Server {
|
||||
state.ecs_mut().insert(ecs_system_metrics);
|
||||
state.ecs_mut().insert(tick_metrics);
|
||||
state.ecs_mut().insert(physics_metrics);
|
||||
if settings.experimental_terrain_persistence {
|
||||
warn!(
|
||||
"Experimental terrain persistence support is enabled. This feature may break, be \
|
||||
disabled, or otherwise change under your feet at *any time*. Additionally, it is \
|
||||
expected to be replaced in the future *without* migration or warning. You have \
|
||||
been warned."
|
||||
);
|
||||
state
|
||||
.ecs_mut()
|
||||
.insert(TerrainPersistence::new(data_dir.to_owned()));
|
||||
}
|
||||
state
|
||||
.ecs_mut()
|
||||
.write_resource::<SlowJobPool>()
|
||||
@ -859,6 +872,12 @@ impl Server {
|
||||
pub fn cleanup(&mut self) {
|
||||
// Cleanup the local state
|
||||
self.state.cleanup();
|
||||
|
||||
// Maintain persisted terrain
|
||||
self.state
|
||||
.ecs()
|
||||
.try_fetch_mut::<TerrainPersistence>()
|
||||
.map(|mut t| t.maintain());
|
||||
}
|
||||
|
||||
fn initialize_client(
|
||||
@ -1236,6 +1255,13 @@ impl Drop for Server {
|
||||
self.metrics_shutdown.notify_one();
|
||||
self.state
|
||||
.notify_players(ServerGeneral::Disconnect(DisconnectReason::Shutdown));
|
||||
self.state
|
||||
.ecs()
|
||||
.try_fetch_mut::<TerrainPersistence>()
|
||||
.map(|mut terrain_persistence| {
|
||||
info!("Unloading terrain persistence...");
|
||||
terrain_persistence.unload_all()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,11 @@ pub struct Settings {
|
||||
pub spawn_town: Option<String>,
|
||||
pub safe_spawn: bool,
|
||||
pub max_player_for_kill_broadcast: Option<usize>,
|
||||
|
||||
/// Experimental feature. No guaranteed forwards-compatibility, may be
|
||||
/// removed at *any time* with no migration.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub experimental_terrain_persistence: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@ -84,6 +89,7 @@ impl Default for Settings {
|
||||
spawn_town: None,
|
||||
safe_spawn: true,
|
||||
max_player_for_kill_broadcast: None,
|
||||
experimental_terrain_persistence: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{client::Client, presence::Presence, Settings};
|
||||
use crate::{client::Client, presence::Presence, Settings, TerrainPersistence};
|
||||
use common::{
|
||||
comp::{
|
||||
Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, Pos, SkillSet,
|
||||
@ -36,6 +36,7 @@ impl Sys {
|
||||
settings: &Read<'_, Settings>,
|
||||
build_areas: &Read<'_, BuildAreas>,
|
||||
player_physics_settings: &mut Write<'_, PlayerPhysicsSettings>,
|
||||
terrain_persistence: &mut Option<Write<'_, TerrainPersistence>>,
|
||||
maybe_player: &Option<&Player>,
|
||||
maybe_admin: &Option<&Admin>,
|
||||
msg: ClientGeneral,
|
||||
@ -190,7 +191,7 @@ impl Sys {
|
||||
if let Some(comp_can_build) = can_build.get(entity) {
|
||||
if comp_can_build.enabled {
|
||||
for area in comp_can_build.build_areas.iter() {
|
||||
if let Some(block) = build_areas
|
||||
if let Some(old_block) = build_areas
|
||||
.areas()
|
||||
.get(*area)
|
||||
// TODO: Make this an exclusive check on the upper bound of the AABB
|
||||
@ -198,13 +199,20 @@ impl Sys {
|
||||
.filter(|aabb| aabb.contains_point(pos))
|
||||
.and_then(|_| terrain.get(pos).ok())
|
||||
{
|
||||
block_changes.set(pos, block.into_vacant());
|
||||
let new_block = old_block.into_vacant();
|
||||
let was_placed = block_changes.try_set(pos, new_block).is_some();
|
||||
if was_placed {
|
||||
if let Some(terrain_persistence) = terrain_persistence.as_mut()
|
||||
{
|
||||
terrain_persistence.set_block(pos, new_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ClientGeneral::PlaceBlock(pos, block) => {
|
||||
ClientGeneral::PlaceBlock(pos, new_block) => {
|
||||
if let Some(comp_can_build) = can_build.get(entity) {
|
||||
if comp_can_build.enabled {
|
||||
for area in comp_can_build.build_areas.iter() {
|
||||
@ -216,7 +224,13 @@ impl Sys {
|
||||
.filter(|aabb| aabb.contains_point(pos))
|
||||
.is_some()
|
||||
{
|
||||
block_changes.try_set(pos, block);
|
||||
let was_placed = block_changes.try_set(pos, new_block).is_some();
|
||||
if was_placed {
|
||||
if let Some(terrain_persistence) = terrain_persistence.as_mut()
|
||||
{
|
||||
terrain_persistence.set_block(pos, new_block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -287,6 +301,7 @@ impl<'a> System<'a> for Sys {
|
||||
Read<'a, Settings>,
|
||||
Read<'a, BuildAreas>,
|
||||
Write<'a, PlayerPhysicsSettings>,
|
||||
Option<Write<'a, TerrainPersistence>>,
|
||||
ReadStorage<'a, Player>,
|
||||
ReadStorage<'a, Admin>,
|
||||
);
|
||||
@ -315,6 +330,7 @@ impl<'a> System<'a> for Sys {
|
||||
settings,
|
||||
build_areas,
|
||||
mut player_physics_settings,
|
||||
mut terrain_persistence,
|
||||
players,
|
||||
admins,
|
||||
): Self::SystemData,
|
||||
@ -349,6 +365,7 @@ impl<'a> System<'a> for Sys {
|
||||
&settings,
|
||||
&build_areas,
|
||||
&mut player_physics_settings,
|
||||
&mut terrain_persistence,
|
||||
&player,
|
||||
&maybe_admin,
|
||||
msg,
|
||||
|
@ -5,7 +5,7 @@ use crate::{
|
||||
presence::{Presence, RepositionOnChunkLoad},
|
||||
rtsim::RtSim,
|
||||
settings::Settings,
|
||||
SpawnPoint, Tick,
|
||||
SpawnPoint, TerrainPersistence, Tick,
|
||||
};
|
||||
use common::{
|
||||
comp::{self, agent, bird_medium, Alignment, BehaviorCapability, ForceUpdate, Pos, Waypoint},
|
||||
@ -99,6 +99,7 @@ impl<'a> System<'a> for Sys {
|
||||
WriteExpect<'a, TerrainGrid>,
|
||||
Write<'a, TerrainChanges>,
|
||||
WriteExpect<'a, RtSim>,
|
||||
Option<WriteExpect<'a, TerrainPersistence>>,
|
||||
WriteStorage<'a, Pos>,
|
||||
ReadStorage<'a, Presence>,
|
||||
ReadStorage<'a, Client>,
|
||||
@ -125,6 +126,7 @@ impl<'a> System<'a> for Sys {
|
||||
mut terrain,
|
||||
mut terrain_changes,
|
||||
mut rtsim,
|
||||
mut terrain_persistence,
|
||||
mut positions,
|
||||
presences,
|
||||
clients,
|
||||
@ -141,7 +143,7 @@ impl<'a> System<'a> for Sys {
|
||||
// Also, send the chunk data to anybody that is close by.
|
||||
let mut new_chunks = Vec::new();
|
||||
'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() {
|
||||
let (chunk, supplement) = match res {
|
||||
let (mut chunk, supplement) = match res {
|
||||
Ok((chunk, supplement)) => (chunk, supplement),
|
||||
Err(Some(entity)) => {
|
||||
if let Some(client) = clients.get(entity) {
|
||||
@ -157,6 +159,11 @@ impl<'a> System<'a> for Sys {
|
||||
},
|
||||
};
|
||||
|
||||
// Apply changes from terrain persistence to this chunk
|
||||
if let Some(terrain_persistence) = terrain_persistence.as_mut() {
|
||||
terrain_persistence.apply_changes(key, &mut chunk);
|
||||
}
|
||||
|
||||
// Arcify the chunk
|
||||
let chunk = Arc::new(chunk);
|
||||
|
||||
@ -409,6 +416,11 @@ impl<'a> System<'a> for Sys {
|
||||
});
|
||||
|
||||
for key in chunks_to_remove {
|
||||
// Register the unloading of this chunk from terrain persistence
|
||||
if let Some(terrain_persistence) = terrain_persistence.as_mut() {
|
||||
terrain_persistence.unload_chunk(key);
|
||||
}
|
||||
|
||||
// TODO: code duplication for chunk insertion between here and state.rs
|
||||
if terrain.remove(key).is_some() {
|
||||
terrain_changes.removed_chunks.insert(key);
|
||||
|
344
server/src/terrain_persistence.rs
Normal file
344
server/src/terrain_persistence.rs
Normal file
@ -0,0 +1,344 @@
|
||||
use atomicwrites::{AtomicFile, OverwriteBehavior};
|
||||
use common::{
|
||||
terrain::{Block, TerrainChunk},
|
||||
vol::{RectRasterableVol, WriteVol},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::{
|
||||
any::{type_name, Any},
|
||||
fs::File,
|
||||
io::{self, Read as _, Write as _},
|
||||
path::PathBuf,
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use vek::*;
|
||||
|
||||
pub struct TerrainPersistence {
|
||||
path: PathBuf,
|
||||
chunks: HashMap<Vec2<i32>, Chunk>,
|
||||
}
|
||||
|
||||
impl TerrainPersistence {
|
||||
/// Create a new terrain persistence system using the given data directory.
|
||||
///
|
||||
/// If the `VELOREN_TERRAIN` environment variable is set, this will be used
|
||||
/// as the persistence directory instead.
|
||||
pub fn new(mut data_dir: PathBuf) -> Self {
|
||||
let path = std::env::var("VELOREN_TERRAIN")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
data_dir.push("terrain");
|
||||
data_dir
|
||||
});
|
||||
|
||||
std::fs::create_dir_all(&path).expect("Failed to create terrain persistence directory");
|
||||
|
||||
info!("Using {:?} as the terrain persistence path", path);
|
||||
|
||||
Self {
|
||||
path,
|
||||
chunks: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply persistence changes to a newly generated chunk.
|
||||
pub fn apply_changes(&mut self, key: Vec2<i32>, terrain_chunk: &mut TerrainChunk) {
|
||||
let chunk = self.load_chunk(key);
|
||||
|
||||
let mut resets = Vec::new();
|
||||
for (rpos, new_block) in chunk.blocks() {
|
||||
if let Err(e) = terrain_chunk.map(rpos, |block| {
|
||||
if block == new_block {
|
||||
resets.push(rpos);
|
||||
}
|
||||
new_block
|
||||
}) {
|
||||
warn!(
|
||||
"Could not set block in chunk {:?} with position {:?} (out of bounds?): {:?}",
|
||||
key, rpos, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset any unchanged blocks (this is an optimisation only)
|
||||
for rpos in resets {
|
||||
chunk.reset_block(rpos);
|
||||
}
|
||||
}
|
||||
|
||||
/// Maintain terrain persistence (writing changes changes back to
|
||||
/// filesystem, etc.)
|
||||
pub fn maintain(&mut self) {
|
||||
// Currently, this does nothing because filesystem writeback occurs on
|
||||
// chunk unload However, this is not a particularly reliable
|
||||
// mechanism (it doesn't survive power loss, say). Later, a more
|
||||
// reliable strategy should be implemented here.
|
||||
}
|
||||
|
||||
fn path_for(&self, key: Vec2<i32>) -> PathBuf {
|
||||
let mut path = self.path.clone();
|
||||
path.push(format!("chunk_{}_{}.dat", key.x, key.y));
|
||||
path
|
||||
}
|
||||
|
||||
fn load_chunk(&mut self, key: Vec2<i32>) -> &mut Chunk {
|
||||
let path = self.path_for(key);
|
||||
self.chunks.entry(key).or_insert_with(|| {
|
||||
File::open(&path)
|
||||
.ok()
|
||||
.map(|f| {
|
||||
let bytes = match std::io::BufReader::new(f)
|
||||
.bytes()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
{
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to read data for chunk {:?} from file: {:?}",
|
||||
key, err
|
||||
);
|
||||
return Chunk::default();
|
||||
},
|
||||
};
|
||||
match Chunk::deserialize_from(std::io::Cursor::new(bytes)) {
|
||||
Some(chunk) => chunk,
|
||||
None => {
|
||||
// Find an untaken name for a backup
|
||||
let mut backup_path = path.clone();
|
||||
backup_path.set_extension("dat_backup_0");
|
||||
let mut i = 1;
|
||||
while backup_path.exists() {
|
||||
backup_path.set_extension(format!("dat_backup_{}", i));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
error!(
|
||||
"Failed to load chunk {:?}, moving possibly corrupt (or too new) \
|
||||
data to {:?} for you to repair.",
|
||||
key, backup_path
|
||||
);
|
||||
if let Err(err) = std::fs::rename(path, backup_path) {
|
||||
error!("Failed to rename invalid chunk file: {:?}", err);
|
||||
}
|
||||
Chunk::default()
|
||||
},
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unload_chunk(&mut self, key: Vec2<i32>) {
|
||||
if let Some(chunk) = self.chunks.remove(&key) {
|
||||
// No need to write if no blocks have ever been written
|
||||
if chunk.blocks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let bytes = match bincode::serialize::<version::Current>(&chunk.prepare_raw()) {
|
||||
Err(err) => {
|
||||
error!("Failed to serialize chunk data: {:?}", err);
|
||||
return;
|
||||
},
|
||||
Ok(bytes) => bytes,
|
||||
};
|
||||
|
||||
let atomic_file =
|
||||
AtomicFile::new(self.path_for(key), OverwriteBehavior::AllowOverwrite);
|
||||
if let Err(err) = atomic_file.write(|file| file.write_all(&bytes)) {
|
||||
error!("Failed to write chunk data to file: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unload_all(&mut self) {
|
||||
for key in self.chunks.keys().copied().collect::<Vec<_>>() {
|
||||
self.unload_chunk(key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_block(&mut self, pos: Vec3<i32>, block: Block) {
|
||||
let key = pos
|
||||
.xy()
|
||||
.map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32));
|
||||
self.load_chunk(key)
|
||||
.blocks
|
||||
.insert(pos - key * TerrainChunk::RECT_SIZE.map(|e| e as i32), block);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerrainPersistence {
|
||||
fn drop(&mut self) { self.unload_all(); }
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct Chunk {
|
||||
blocks: HashMap<Vec3<i32>, Block>,
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
fn deserialize_from<R: io::Read + Clone>(reader: R) -> Option<Self> {
|
||||
version::try_load(reader)
|
||||
}
|
||||
|
||||
fn prepare_raw(self) -> version::Current { self.into() }
|
||||
|
||||
fn blocks(&self) -> impl Iterator<Item = (Vec3<i32>, Block)> + '_ {
|
||||
self.blocks.iter().map(|(k, b)| (*k, *b))
|
||||
}
|
||||
|
||||
fn reset_block(&mut self, rpos: Vec3<i32>) { self.blocks.remove(&rpos); }
|
||||
}
|
||||
|
||||
/// # Adding a new chunk format version
|
||||
///
|
||||
/// Chunk formats are designed to be backwards-compatible when loading, but are
|
||||
/// not required to be backwards-compatible when saving (i.e: we must always be
|
||||
/// able to load old formats, but we're not required to save old formats because
|
||||
/// newer formats might contain richer information that is incompatible with an
|
||||
/// older format).
|
||||
///
|
||||
/// The steps for doing this are as follows:
|
||||
///
|
||||
/// 1. Create a new 'raw format' type that implements [`Serialize`] and
|
||||
/// `Deserialize`]. Make sure to add a version field. If in doubt, copy the last
|
||||
/// raw format and increment the version number wherever it appears. Don't
|
||||
/// forget to increment the version number in the `serde(deserialize_with =
|
||||
/// ...}` attribute! Conventionally, these types are named `V{N}` where `{N}` is
|
||||
/// the number succeeding the previous raw format type.
|
||||
///
|
||||
/// 2. Add an implementation of `From<{YourRawFormat}>` for `Chunk`. As before,
|
||||
/// see previous versions if in doubt.
|
||||
///
|
||||
/// 3. Change the type of [`version::Current`] to your new raw format type.
|
||||
///
|
||||
/// 4. Add an entry for your raw format at the top of the array in
|
||||
/// [`version::loaders`].
|
||||
///
|
||||
/// 5. Remove the `Serialize` implementation from the previous raw format type:
|
||||
/// we don't need it any longer!
|
||||
mod version {
|
||||
use super::*;
|
||||
|
||||
/// The newest supported raw format type. This should be changed every time
|
||||
/// a new raw format is added.
|
||||
// Step [3]
|
||||
pub type Current = V3;
|
||||
|
||||
type LoadChunkFn<R> = fn(R) -> Result<Chunk, (&'static str, bincode::Error)>;
|
||||
fn loaders<'a, R: io::Read + Clone>() -> &'a [LoadChunkFn<R>] {
|
||||
// Step [4]
|
||||
&[load_raw::<V3, _>, load_raw::<V2, _>, load_raw::<V1, _>]
|
||||
}
|
||||
|
||||
// Convert back to current
|
||||
|
||||
impl From<Chunk> for Current {
|
||||
fn from(chunk: Chunk) -> Self {
|
||||
Self {
|
||||
version: version_magic(3),
|
||||
blocks: chunk
|
||||
.blocks
|
||||
.into_iter()
|
||||
.map(|(pos, b)| (pos.x as u8, pos.y as u8, pos.z as i16, b.to_u32()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Version 3 of the raw chunk format.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct V3 {
|
||||
#[serde(deserialize_with = "version::<_, 3>")]
|
||||
pub version: u64,
|
||||
pub blocks: Vec<(u8, u8, i16, u32)>,
|
||||
}
|
||||
|
||||
impl From<V3> for Chunk {
|
||||
fn from(v3: V3) -> Self {
|
||||
Self {
|
||||
blocks: v3
|
||||
.blocks
|
||||
.into_iter()
|
||||
.map(|(x, y, z, b)| {
|
||||
(
|
||||
Vec3::new(x as i32, y as i32, z as i32),
|
||||
Block::from_u32(b).unwrap_or_else(Block::empty),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Version 2 of the raw chunk format.
|
||||
#[derive(Deserialize)]
|
||||
pub struct V2 {
|
||||
#[serde(deserialize_with = "version::<_, 2>")]
|
||||
pub version: u64,
|
||||
pub blocks: Vec<(u8, u8, i16, Block)>,
|
||||
}
|
||||
|
||||
impl From<V2> for Chunk {
|
||||
fn from(v2: V2) -> Self {
|
||||
Self {
|
||||
blocks: v2
|
||||
.blocks
|
||||
.into_iter()
|
||||
.map(|(x, y, z, b)| (Vec3::new(x as i32, y as i32, z as i32), b))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Version 1 of the raw chunk format.
|
||||
#[derive(Deserialize)]
|
||||
pub struct V1 {
|
||||
pub blocks: HashMap<Vec3<i32>, Block>,
|
||||
}
|
||||
|
||||
impl From<V1> for Chunk {
|
||||
fn from(v1: V1) -> Self { Self { blocks: v1.blocks } }
|
||||
}
|
||||
|
||||
// Utility things
|
||||
|
||||
fn version_magic(n: u16) -> u64 { (n as u64) | (0x3352ACEEA789 << 16) }
|
||||
|
||||
fn version<'de, D: serde::Deserializer<'de>, const V: u16>(de: D) -> Result<u64, D::Error> {
|
||||
u64::deserialize(de).and_then(|x| {
|
||||
if x == version_magic(V) {
|
||||
Ok(x)
|
||||
} else {
|
||||
Err(serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Unsigned(x),
|
||||
&"incorrect magic/version bytes",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn load_raw<RawChunk: Any + Into<Chunk> + DeserializeOwned, R: io::Read + Clone>(
|
||||
reader: R,
|
||||
) -> Result<Chunk, (&'static str, bincode::Error)> {
|
||||
bincode::deserialize_from::<_, RawChunk>(reader)
|
||||
.map(Into::into)
|
||||
.map_err(|e| (type_name::<RawChunk>(), e))
|
||||
}
|
||||
|
||||
pub fn try_load<R: io::Read + Clone>(reader: R) -> Option<Chunk> {
|
||||
loaders()
|
||||
.iter()
|
||||
.find_map(|load_raw| match load_raw(reader.clone()) {
|
||||
Ok(chunk) => Some(chunk),
|
||||
Err((raw_name, e)) => {
|
||||
debug!(
|
||||
"Attempt to load chunk with raw format `{}` failed: {:?}",
|
||||
raw_name, e
|
||||
);
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user