-
-
Notifications
You must be signed in to change notification settings - Fork 186
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
feat(player-data): Support saving and loading player NBT Data #600
base: master
Are you sure you want to change the base?
Conversation
/// Is Player Data saving enabled? | ||
pub save_player_data: bool, | ||
/// Is Player Data should be cached? | ||
pub cache_player_data: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to cache player data, just read it back from disk when they rejoin
pub fn read_gzip_compound_tag(compressed_data: &[u8]) -> Result<NbtCompound, Error> { | ||
// Create a GZip decoder | ||
let mut decoder = GzDecoder::new(compressed_data); | ||
|
||
// Decompress the data | ||
let mut decompressed_data = Vec::new(); | ||
decoder | ||
.read_to_end(&mut decompressed_data) | ||
.map_err(Error::Incomplete)?; | ||
|
||
// Read the NBT data | ||
let nbt = Nbt::read(&mut deserializer::ReadAdaptor::new(&decompressed_data[..]))?; | ||
Ok(nbt.root_tag) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should take an impl Read
, then you can:
Nbt::read(&mut ReadAdaptor::new(GzDecoder::new(input)))
and skip having to buffer the data.
pub fn write_gzip_compound_tag(compound: &NbtCompound) -> Result<Vec<u8>, Error> { | ||
// First serialize the NBT data | ||
let nbt = Nbt::new(String::new(), compound.clone()); | ||
let serialized = nbt.write(); | ||
|
||
// Then compress it with GZip | ||
let mut compressed_data = Vec::new(); | ||
{ | ||
let mut encoder = GzEncoder::new(&mut compressed_data, Compression::default()); | ||
encoder.write_all(&serialized).map_err(Error::Incomplete)?; | ||
encoder.finish().map_err(Error::Incomplete)?; | ||
} | ||
|
||
Ok(compressed_data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should add a write_to(impl Write
for Nbt and chain the writers like the readers above to avoid having to write to a buffer.
pub fn from_gzip_bytes<'a, T>(compressed_data: &[u8]) -> Result<T, Error> | ||
where | ||
T: serde::Deserialize<'a>, | ||
{ | ||
// Create a GZip decoder | ||
let mut decoder = GzDecoder::new(compressed_data); | ||
|
||
// Decompress the data | ||
let mut decompressed_data = Vec::new(); | ||
decoder | ||
.read_to_end(&mut decompressed_data) | ||
.map_err(Error::Incomplete)?; | ||
|
||
// Deserialize the NBT data | ||
deserializer::from_bytes(&decompressed_data[..]) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't write to a buffer here, just chain the readers
pub fn to_gzip_bytes<T>(value: &T) -> Result<Vec<u8>, Error> | ||
where | ||
T: serde::Serialize, | ||
{ | ||
// First serialize the NBT data | ||
let mut uncompressed_data = Vec::new(); | ||
serializer::to_bytes(value, &mut uncompressed_data)?; | ||
|
||
// Then compress it with GZip | ||
let mut compressed_data = Vec::new(); | ||
{ | ||
let mut encoder = GzEncoder::new(&mut compressed_data, Compression::default()); | ||
encoder | ||
.write_all(&uncompressed_data) | ||
.map_err(Error::Incomplete)?; | ||
encoder.finish().map_err(Error::Incomplete)?; | ||
} | ||
|
||
Ok(compressed_data) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't write to a buffer here, just chain the writers.
/// Path to the directory where player data is stored | ||
data_path: PathBuf, | ||
/// In-memory cache of recently disconnected players' data | ||
cache: Mutex<HashMap<Uuid, (NbtCompound, Instant)>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this really need to be a mutex? Can you have PlayerDataStorage
be mutable when you need to modify it?
pub struct ServerPlayerData { | ||
storage: Arc<PlayerDataStorage>, | ||
save_interval: Duration, | ||
last_cleanup: Mutex<Instant>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these need to be mutexs? can server player data be mutable when you need to modify them?
match self { | ||
Self::Io(err) => Some(format!("Failed to load player data: {err}")), | ||
Self::Nbt(err) => Some(format!("Failed to parse player data: {err}")), | ||
Self::NotFound(uuid) => Some(format!("Player data not found for UUID: {uuid}")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the player data is not found, shouldn't a new file be created?
@@ -137,6 +139,12 @@ impl Server { | |||
server_listing: Mutex::new(CachedStatus::new()), | |||
server_branding: CachedBranding::new(), | |||
bossbars: Mutex::new(CustomBossbars::new()), | |||
player_data_storage: ServerPlayerData::new( | |||
"./world/playerdata", // TODO: handle world name in config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should make a helper function for getting the world name and feed that here and where the chunk loading gets the world name
Why not move the nbt data into the hunger manager |
Description
This PR adds the must-have feature to save and load player NBT Data !
This code actually does:
This code also contains a rework of the experience packet and health packet with
tick_experience
andtick_health
; this is needed with how NBT data are handled before player spawning in the world. Like that, we send experience, health, ... directly on the first player tick and send them when there is an update like before.I also added some data to NBT, like gamemode, or food, and impl them in the code to use them now instead of default data. Now the future part needed for this code is the implementation of other NBT values accessible from https://minecraft.wiki/w/Player.dat_format
Testing
I have tested to move on the map, give experience, change gamemode, ... stopping the server and joining again and all is working good. May need more test from others !
Please follow our Coding Guidelines