Skip to content

Commit

Permalink
Anvil world persistence (feather-rs#145)
Browse files Browse the repository at this point in the history
Resolves feather-rs#52.

Features implemented:
* Sector allocator for region files
* RegionFile::save_chunk()
* Chunk saving support for chunk worker
* Periodic saving of loaded chunks (only those modified), at configurable intervals
* Saving of chunks on unload and shutdown
* Creation of new region files
* Saving of entities
* Saving of player data on disconnect and shutdown
* Saving of level data

This is a squashing of 23 commits:

* Implement sector allocator

* Implement conversion of chunk data compound to `nbt::Blob`

* Initial implementation of RegionHandle::save_chunk (untested)

* Initial chunk worker support for chunk saving

* Save chunks upon unload

* Implement periodic saving of chunks, with interval defined in config

* Implement saving of chunks on shutdown

* Implement creation of new region files

* Save level.dat on shutdown

* Begin work on entity saving

* Fix saving of item entities

* Add more data to chunk save so that the vanilla server accepts it

* Fix panic when chunk containing players was saved

* Attempt to fix Windows overflow error

* Implement saving of player data on disconnect and shutdown

* Fix ordering of pitch and yaw

* Fix panic on double chunk unload

* Fix panic when entities without position are saved

* Use more idiomatic early return syntax

* Implement conversion from global to section palette for chunk saving
  • Loading branch information
caelunshun authored and cheako committed Jan 4, 2020
1 parent 84352bc commit f0bf9d1
Show file tree
Hide file tree
Showing 26 changed files with 1,696 additions and 259 deletions.
305 changes: 182 additions & 123 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ hash32-derive = "0.1.0"
strum = "0.15.0"
strum_macros = "0.15.0"
tokio = "=0.2.0-alpha.6"
failure = "0.1.5"
failure = "0.1.5"
bitvec = "0.15.2"
multimap = "0.6.0"
5 changes: 5 additions & 0 deletions core/src/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ impl Inventory {
pub fn slot_count(&self) -> u16 {
self.items.len() as u16
}

/// Returns a reference to this inventory's items.
pub fn items(&self) -> &[Option<ItemStack>] {
&self.items
}
}

/// Represents an item stack.
Expand Down
76 changes: 76 additions & 0 deletions core/src/save/entity.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::Position;
use nbt::Value;
use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "id")]
Expand All @@ -13,6 +15,32 @@ pub enum EntityData {
Unknown,
}

impl EntityData {
pub fn into_nbt_value(self) -> Value {
let mut map = HashMap::new();

map.insert(
String::from("id"),
Value::String(
match self {
EntityData::Item(_) => "minecraft:item",
EntityData::Arrow(_) => "minecraft:arrow",
EntityData::Unknown => panic!("Cannot write unknown entities"),
}
.to_string(),
),
);

match self {
EntityData::Item(data) => data.write_to_map(&mut map),
EntityData::Arrow(data) => data.write_to_map(&mut map),
EntityData::Unknown => panic!("Cannot write unknown entities"),
}

Value::Compound(map)
}
}

/// Common entity tags.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseEntityData {
Expand All @@ -24,6 +52,23 @@ pub struct BaseEntityData {
pub velocity: Vec<f64>,
}

impl BaseEntityData {
fn write_to_map(self, map: &mut HashMap<String, Value>) {
map.insert(
String::from("Pos"),
Value::List(self.position.into_iter().map(Value::Double).collect()),
);
map.insert(
String::from("Rotation"),
Value::List(self.rotation.into_iter().map(Value::Float).collect()),
);
map.insert(
String::from("Motion"),
Value::List(self.velocity.into_iter().map(Value::Double).collect()),
);
}
}

impl BaseEntityData {
/// Reads the position and rotation fields. If the fields are invalid, None is returned.
pub fn read_position(self: &BaseEntityData) -> Option<Position> {
Expand Down Expand Up @@ -74,6 +119,13 @@ pub struct ItemData {
pub item: String,
}

impl ItemData {
fn write_to_map(self, map: &mut HashMap<String, Value>) {
map.insert(String::from("Count"), Value::Byte(self.count as i8));
map.insert(String::from("id"), Value::String(self.item));
}
}

/// Data for an Item entity (`minecraft:item`).
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
pub struct ItemEntityData {
Expand All @@ -90,6 +142,22 @@ pub struct ItemEntityData {
pub item: ItemData,
}

impl ItemEntityData {
fn write_to_map(self, map: &mut HashMap<String, Value>) {
self.entity.write_to_map(map);

let mut item = HashMap::new();
self.item.write_to_map(&mut item);
map.insert(String::from("Item"), Value::Compound(item));

map.insert(String::from("Age"), Value::Short(self.age));
map.insert(
String::from("PickupDelay"),
Value::Byte(self.pickup_delay as i8),
);
}
}

/// Data for an Arrow entity (`minecraft:arrow`).
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ArrowEntityData {
Expand All @@ -105,6 +173,14 @@ pub struct ArrowEntityData {
pub critical: u8,
}

impl ArrowEntityData {
fn write_to_map(self, map: &mut HashMap<String, Value>) {
self.entity.write_to_map(map);

map.insert(String::from("crit"), Value::Byte(self.critical as i8));
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
62 changes: 56 additions & 6 deletions core/src/save/player_data.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::fs::File;

use crate::entity::BaseEntityData;
use crate::inventory::SlotIndex;
use crate::inventory::{
SlotIndex, HOTBAR_SIZE, INVENTORY_SIZE, SLOT_ARMOR_MAX, SLOT_ARMOR_MIN, SLOT_HOTBAR_OFFSET,
SLOT_INVENTORY_OFFSET, SLOT_OFFHAND,
};
use crate::ItemStack;
use feather_items::Item;
use std::io::Read;
use std::path::Path;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;

/// Represents the contents of a player data file.
Expand All @@ -22,7 +26,7 @@ pub struct PlayerData {
}

/// Represents a single inventory slot (including position index).
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InventorySlot {
#[serde(rename = "Count")]
pub count: i8,
Expand All @@ -41,6 +45,31 @@ impl InventorySlot {
}
}

/// Converts a network protocol index, item, and count
/// to an `InventorySlot`.
pub fn from_network_index(network: SlotIndex, stack: ItemStack) -> Self {
let slot = if SLOT_HOTBAR_OFFSET <= network && network < SLOT_HOTBAR_OFFSET + HOTBAR_SIZE {
// Hotbar
(network - SLOT_HOTBAR_OFFSET) as i8
} else if network == SLOT_OFFHAND {
-106
} else if SLOT_ARMOR_MIN <= network && network <= SLOT_ARMOR_MAX {
((SLOT_ARMOR_MAX - network) + 100) as i8
} else if SLOT_INVENTORY_OFFSET <= network
&& network < SLOT_INVENTORY_OFFSET + INVENTORY_SIZE
{
network as i8
} else {
panic!("Invalid slot index {} on server", network);
};

Self {
count: stack.amount as i8,
slot,
item: stack.ty.identifier().to_string(),
}
}

/// Converts an NBT inventory index to a network protocol index.
/// Returns None if the index is invalid.
pub fn convert_index(&self) -> Option<SlotIndex> {
Expand Down Expand Up @@ -68,12 +97,27 @@ fn load_from_file<R: Read>(reader: R) -> Result<PlayerData, nbt::Error> {
}

pub fn load_player_data(world_dir: &Path, uuid: Uuid) -> Result<PlayerData, nbt::Error> {
let file_path = world_dir.join("playerdata").join(format!("{}.dat", uuid));
let file_path = file_path(world_dir, uuid);
let file = File::open(file_path)?;
let data = load_from_file(file)?;
Ok(data)
}

fn save_to_file<W: Write>(mut writer: W, data: PlayerData) -> Result<(), nbt::Error> {
nbt::to_gzip_writer(&mut writer, &data, None)
}

pub fn save_player_data(world_dir: &Path, uuid: Uuid, data: PlayerData) -> Result<(), nbt::Error> {
fs::create_dir_all(world_dir.join("playerdata"))?;
let file_path = file_path(world_dir, uuid);
let file = File::create(file_path)?;
save_to_file(file, data)
}

fn file_path(world_dir: &Path, uuid: Uuid) -> PathBuf {
world_dir.join("playerdata").join(format!("{}.dat", uuid))
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -135,14 +179,20 @@ mod tests {
map.insert(x, x as usize);
}

dbg!(map.clone());

// Check all valid slots
for (src, expected) in map {
let slot = InventorySlot {
slot: src,
count: 1,
item: String::from("invalid:identifier"),
item: String::from(Item::Stone.identifier()),
};
assert_eq!(slot.convert_index().unwrap(), expected);
assert_eq!(
InventorySlot::from_network_index(expected, ItemStack::new(Item::Stone, 1)),
slot
);
}

// Check that invalid slots error out
Expand Down
Loading

0 comments on commit f0bf9d1

Please sign in to comment.