1mod file_storage;
4mod known_players;
5mod memory_storage;
6mod permissions;
7mod stats;
8
9#[cfg(test)]
10mod tests;
11
12use std::{io::Cursor, path::PathBuf};
13
14use simdnbt::{ToNbtTag, borrow::read_compound as read_borrowed_compound, owned::NbtTag};
15use tokio::io;
16use uuid::Uuid;
17use wincode::{SchemaRead, SchemaWrite};
18
19#[cfg(test)]
20use self::permissions::set_permission_subject;
21use super::PlayerRespawnConfig;
22use super::player_data::{
23 PLAYER_DATA_VERSION, PersistentAbilities, PersistentEnderPearl, PersistentPlayerData,
24 PersistentRootVehicle, PersistentSlot,
25};
26use crate::chunk_saver::PersistentEntity;
27use crate::config::StorageSelection;
28use crate::level_data::RespawnData;
29use crate::permission::PermissionSubjectIndex;
30#[cfg(test)]
31use crate::permission::PermissionSubjectState;
32use crate::player::KnownPlayers;
33use crate::player::Player;
34use crate::player::player_data_storage::file_storage::FilePlayerDataStorage;
35use crate::player::player_data_storage::memory_storage::MemoryPlayerDataStorage;
36use steel_registry::item_stack::ItemStack;
37use steel_utils::{BlockPos, Identifier};
38
39const PLAYER_MAGIC: [u8; 4] = *b"STLP";
40const GLOBAL_MAGIC: [u8; 4] = *b"STLG";
41const PLAYER_STORAGE_VERSION: u16 = 9;
42const GLOBAL_STORAGE_VERSION: u16 = 1;
43const GLOBAL_PLAYER_DATA_VERSION: i32 = 1;
44
45#[derive(Debug, Clone)]
47pub struct GlobalPlayerData {
48 pub last_active_domain: String,
50}
51
52pub struct PlayerDataStorage {
54 backend: PlayerDataStorageBackend,
55}
56
57enum PlayerDataStorageBackend {
58 File(FilePlayerDataStorage),
59 Memory(MemoryPlayerDataStorage),
60}
61
62#[derive(SchemaWrite, SchemaRead)]
63struct PlayerDataFile {
64 data_version: i32,
65 pos: [f64; 3],
66 motion: [f64; 3],
67 rotation: [f32; 2],
68 on_ground: bool,
69 fall_flying: bool,
70 remaining_fire_ticks: i32,
71 ticks_frozen: i32,
72 is_in_powder_snow: bool,
73 was_in_powder_snow: bool,
74 has_visual_fire: bool,
75 health: f32,
76 game_mode: i32,
77 prev_game_mode: Option<i32>,
78 abilities: AbilitiesFile,
79 inventory: Vec<SlotFile>,
80 selected_slot: i32,
81 world: String,
82 food_level: i32,
83 food_saturation_level: f32,
84 food_exhaustion_level: f32,
85 food_tick_timer: i32,
86 experience_level: i32,
87 experience_progress: f32,
88 experience_total: i32,
89 score: i32,
90 seen_credits: bool,
91 root_vehicle: Option<RootVehicleFile>,
92 respawn_config: Option<RespawnConfigFile>,
93 ender_pearls: Vec<EnderPearlFile>,
94 ender_items: Vec<SlotFile>,
95}
96
97#[derive(SchemaWrite, SchemaRead)]
98struct RootVehicleFile {
99 attach: [u8; 16],
100 entity: PersistentEntity,
101}
102
103#[derive(SchemaWrite, SchemaRead)]
104struct RespawnConfigFile {
105 dimension: String,
106 pos: [i32; 3],
107 yaw: f32,
108 pitch: f32,
109 forced: bool,
110}
111
112#[derive(SchemaWrite, SchemaRead)]
113struct EnderPearlFile {
114 world: String,
115 entity: PersistentEntity,
116}
117
118#[derive(SchemaWrite, SchemaRead)]
119struct AbilitiesFile {
120 invulnerable: bool,
121 flying: bool,
122 may_fly: bool,
123 instabuild: bool,
124 may_build: bool,
125 flying_speed: f32,
126 walking_speed: f32,
127}
128
129#[derive(SchemaWrite, SchemaRead)]
130struct SlotFile {
131 slot: i8,
132 item_nbt: Vec<u8>,
133}
134
135#[derive(SchemaWrite, SchemaRead)]
136struct GlobalPlayerDataFile {
137 data_version: i32,
138 last_active_domain: String,
139}
140
141impl PlayerDataStorage {
142 pub async fn from_selection(
146 save_root: PathBuf,
147 selection: &StorageSelection,
148 ) -> io::Result<Self> {
149 if selection.kind == Identifier::from_steel("file") {
150 Self::on_disk(save_root).await
151 } else if selection.kind == Identifier::from_steel("ram") {
152 Ok(Self::in_memory())
153 } else {
154 Err(io::Error::new(
155 io::ErrorKind::InvalidInput,
156 format!("unknown player storage {}", selection.kind),
157 ))
158 }
159 }
160
161 pub async fn on_disk(save_root: PathBuf) -> io::Result<Self> {
163 let backend = PlayerDataStorageBackend::File(FilePlayerDataStorage::new(save_root).await?);
164 Ok(Self { backend })
165 }
166
167 #[must_use]
169 pub fn in_memory() -> Self {
170 Self {
171 backend: PlayerDataStorageBackend::Memory(MemoryPlayerDataStorage::default()),
172 }
173 }
174
175 pub async fn save(&self, player: &Player) -> io::Result<()> {
177 let domain = player.get_world().domain().to_owned();
178 self.save_domain(&domain, player).await?;
179 self.save_global(
180 player.gameprofile.id,
181 &GlobalPlayerData {
182 last_active_domain: domain,
183 },
184 )
185 .await
186 }
187
188 pub async fn save_domain(&self, domain: &str, player: &Player) -> io::Result<()> {
190 match &self.backend {
191 PlayerDataStorageBackend::File(storage) => storage.save_domain(domain, player).await,
192 PlayerDataStorageBackend::Memory(storage) => {
193 storage.save_domain(domain, player);
194 Ok(())
195 }
196 }
197 }
198
199 pub async fn save_domain_data(
201 &self,
202 domain: &str,
203 uuid: Uuid,
204 data: &PersistentPlayerData,
205 ) -> io::Result<()> {
206 match &self.backend {
207 PlayerDataStorageBackend::File(storage) => {
208 storage.save_domain_data(domain, uuid, data).await
209 }
210 PlayerDataStorageBackend::Memory(storage) => {
211 storage.save_domain_data(domain, uuid, data);
212 Ok(())
213 }
214 }
215 }
216
217 pub async fn load_domain(
219 &self,
220 domain: &str,
221 uuid: Uuid,
222 ) -> io::Result<Option<PersistentPlayerData>> {
223 match &self.backend {
224 PlayerDataStorageBackend::File(storage) => storage.load_domain(domain, uuid).await,
225 PlayerDataStorageBackend::Memory(storage) => Ok(storage.load_domain(domain, uuid)),
226 }
227 }
228
229 pub async fn load_global(&self, uuid: Uuid) -> io::Result<Option<GlobalPlayerData>> {
231 match &self.backend {
232 PlayerDataStorageBackend::File(storage) => storage.load_global(uuid).await,
233 PlayerDataStorageBackend::Memory(storage) => Ok(storage.load_global(uuid)),
234 }
235 }
236
237 pub async fn load_permission_subjects(&self) -> io::Result<PermissionSubjectIndex> {
239 match &self.backend {
240 PlayerDataStorageBackend::File(storage) => storage.load_permission_subjects().await,
241 PlayerDataStorageBackend::Memory(storage) => Ok(storage.load_permission_subjects()),
242 }
243 }
244
245 pub async fn load_known_players(&self) -> io::Result<KnownPlayers> {
247 match &self.backend {
248 PlayerDataStorageBackend::File(storage) => storage.load_known_players().await,
249 PlayerDataStorageBackend::Memory(storage) => Ok(storage.load_known_players()),
250 }
251 }
252
253 pub async fn save_known_players_if_current(
255 &self,
256 players: &KnownPlayers,
257 is_current: impl FnOnce() -> bool + Send,
258 ) -> io::Result<bool> {
259 match &self.backend {
260 PlayerDataStorageBackend::File(storage) => {
261 storage
262 .save_known_players_if_current(players, is_current)
263 .await
264 }
265 PlayerDataStorageBackend::Memory(storage) => {
266 Ok(storage.save_known_players_if_current(players, is_current))
267 }
268 }
269 }
270
271 pub async fn save_global(&self, uuid: Uuid, data: &GlobalPlayerData) -> io::Result<()> {
273 match &self.backend {
274 PlayerDataStorageBackend::File(storage) => storage.save_global(uuid, data).await,
275 PlayerDataStorageBackend::Memory(storage) => {
276 storage.save_global(uuid, data);
277 Ok(())
278 }
279 }
280 }
281
282 pub async fn save_permission_subjects(
284 &self,
285 subjects: &PermissionSubjectIndex,
286 ) -> io::Result<()> {
287 match &self.backend {
288 PlayerDataStorageBackend::File(storage) => {
289 storage.save_permission_subjects(subjects).await
290 }
291 PlayerDataStorageBackend::Memory(storage) => {
292 storage.save_permission_subjects(subjects);
293 Ok(())
294 }
295 }
296 }
297}
298
299impl PlayerDataFile {
300 fn from_persistent(data: &PersistentPlayerData) -> io::Result<Self> {
301 let mut inventory = Vec::with_capacity(data.inventory.len());
302 for slot in &data.inventory {
303 inventory.push(SlotFile {
304 slot: slot.slot,
305 item_nbt: item_to_nbt_bytes(&slot.item)?,
306 });
307 }
308
309 Ok(Self {
310 data_version: data.data_version,
311 pos: data.pos,
312 motion: data.motion,
313 rotation: data.rotation,
314 on_ground: data.on_ground,
315 fall_flying: data.fall_flying,
316 remaining_fire_ticks: data.remaining_fire_ticks,
317 ticks_frozen: data.ticks_frozen,
318 is_in_powder_snow: data.is_in_powder_snow,
319 was_in_powder_snow: data.was_in_powder_snow,
320 has_visual_fire: data.has_visual_fire,
321 health: data.health,
322 game_mode: data.game_mode,
323 prev_game_mode: data.prev_game_mode,
324 abilities: AbilitiesFile {
325 invulnerable: data.abilities.invulnerable,
326 flying: data.abilities.flying,
327 may_fly: data.abilities.may_fly,
328 instabuild: data.abilities.instabuild,
329 may_build: data.abilities.may_build,
330 flying_speed: data.abilities.flying_speed,
331 walking_speed: data.abilities.walking_speed,
332 },
333 inventory,
334 selected_slot: data.selected_slot,
335 world: data.world.clone(),
336 food_level: data.food_level,
337 food_saturation_level: data.food_saturation_level,
338 food_exhaustion_level: data.food_exhaustion_level,
339 food_tick_timer: data.food_tick_timer,
340 experience_level: data.experience_level,
341 experience_progress: data.experience_progress,
342 experience_total: data.experience_total,
343 score: data.score,
344 seen_credits: data.seen_credits,
345 root_vehicle: data
346 .root_vehicle
347 .clone()
348 .map(|root_vehicle| RootVehicleFile {
349 attach: root_vehicle.attach,
350 entity: root_vehicle.entity,
351 }),
352 respawn_config: data
353 .respawn_config
354 .clone()
355 .map(RespawnConfigFile::from_runtime),
356 ender_pearls: data
357 .ender_pearls
358 .iter()
359 .map(|pearl| EnderPearlFile {
360 world: pearl.world.clone(),
361 entity: pearl.entity.clone(),
362 })
363 .collect(),
364 ender_items: data
365 .ender_items
366 .iter()
367 .map(|slot| SlotFile {
368 slot: slot.slot,
369 item_nbt: item_to_nbt_bytes(&slot.item).unwrap_or_default(),
370 })
371 .collect(),
372 })
373 }
374
375 fn into_persistent(self) -> io::Result<PersistentPlayerData> {
376 if self.data_version != PLAYER_DATA_VERSION {
377 return Err(io::Error::new(
378 io::ErrorKind::InvalidData,
379 format!(
380 "unsupported player data payload version {}",
381 self.data_version
382 ),
383 ));
384 }
385
386 let mut inventory = Vec::with_capacity(self.inventory.len());
387 for slot in self.inventory {
388 inventory.push(PersistentSlot {
389 slot: slot.slot,
390 item: item_from_nbt_bytes(&slot.item_nbt)?,
391 });
392 }
393
394 Ok(PersistentPlayerData {
395 pos: self.pos,
396 motion: self.motion,
397 rotation: self.rotation,
398 on_ground: self.on_ground,
399 fall_flying: self.fall_flying,
400 remaining_fire_ticks: self.remaining_fire_ticks,
401 ticks_frozen: self.ticks_frozen,
402 is_in_powder_snow: self.is_in_powder_snow,
403 was_in_powder_snow: self.was_in_powder_snow,
404 has_visual_fire: self.has_visual_fire,
405 health: self.health,
406 game_mode: self.game_mode,
407 prev_game_mode: self.prev_game_mode,
408 abilities: PersistentAbilities {
409 invulnerable: self.abilities.invulnerable,
410 flying: self.abilities.flying,
411 may_fly: self.abilities.may_fly,
412 instabuild: self.abilities.instabuild,
413 may_build: self.abilities.may_build,
414 flying_speed: self.abilities.flying_speed,
415 walking_speed: self.abilities.walking_speed,
416 },
417 inventory,
418 selected_slot: self.selected_slot,
419 world: self.world,
420 food_level: self.food_level,
421 food_saturation_level: self.food_saturation_level,
422 food_exhaustion_level: self.food_exhaustion_level,
423 food_tick_timer: self.food_tick_timer,
424 data_version: self.data_version,
425 experience_level: self.experience_level,
426 experience_progress: self.experience_progress,
427 experience_total: self.experience_total,
428 score: self.score,
429 seen_credits: self.seen_credits,
430 root_vehicle: self.root_vehicle.map(|root_vehicle| PersistentRootVehicle {
431 attach: root_vehicle.attach,
432 entity: root_vehicle.entity,
433 }),
434 respawn_config: self
435 .respawn_config
436 .map(RespawnConfigFile::into_runtime)
437 .transpose()?,
438 ender_pearls: self
439 .ender_pearls
440 .into_iter()
441 .map(|pearl| PersistentEnderPearl {
442 world: pearl.world,
443 entity: pearl.entity,
444 })
445 .collect(),
446 ender_items: {
447 let mut items = Vec::with_capacity(self.ender_items.len());
448 for slot in self.ender_items {
449 items.push(PersistentSlot {
450 slot: slot.slot,
451 item: item_from_nbt_bytes(&slot.item_nbt)?,
452 });
453 }
454 items
455 },
456 stats: Vec::new(),
457 })
458 }
459}
460
461impl RespawnConfigFile {
462 fn from_runtime(config: PlayerRespawnConfig) -> Self {
463 let pos = config.respawn_data.pos();
464 Self {
465 dimension: config.respawn_data.dimension().to_string(),
466 pos: [pos.x(), pos.y(), pos.z()],
467 yaw: config.respawn_data.yaw,
468 pitch: config.respawn_data.pitch,
469 forced: config.forced,
470 }
471 }
472
473 fn into_runtime(self) -> io::Result<PlayerRespawnConfig> {
474 Ok(PlayerRespawnConfig {
475 respawn_data: RespawnData::of(
476 self.dimension.parse().map_err(|error| {
477 io::Error::new(
478 io::ErrorKind::InvalidData,
479 format!("invalid respawn dimension: {error}"),
480 )
481 })?,
482 BlockPos::new(self.pos[0], self.pos[1], self.pos[2]),
483 self.yaw,
484 self.pitch,
485 ),
486 forced: self.forced,
487 })
488 }
489}
490
491fn item_to_nbt_bytes(item: &ItemStack) -> io::Result<Vec<u8>> {
492 let NbtTag::Compound(compound) = item.clone().to_nbt_tag() else {
493 return Err(io::Error::new(
494 io::ErrorKind::InvalidData,
495 "item stack did not serialize to a compound",
496 ));
497 };
498 let mut bytes = Vec::new();
499 compound.write(&mut bytes);
500 Ok(bytes)
501}
502
503fn item_from_nbt_bytes(bytes: &[u8]) -> io::Result<ItemStack> {
504 let nbt = read_borrowed_compound(&mut Cursor::new(bytes)).map_err(|e| {
505 io::Error::new(
506 io::ErrorKind::InvalidData,
507 format!("failed to parse item NBT: {e}"),
508 )
509 })?;
510 let compound = simdnbt::borrow::NbtCompound::from(&nbt);
511 ItemStack::from_borrowed_compound(&compound)
512 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid item stack data"))
513}
514
515fn encode_player_file(file: &PlayerDataFile) -> io::Result<Vec<u8>> {
516 encode_file(
517 PLAYER_MAGIC,
518 PLAYER_STORAGE_VERSION,
519 wincode::serialize(file),
520 )
521}
522
523fn decode_player_file(bytes: &[u8]) -> io::Result<PlayerDataFile> {
524 let payload = decode_file(PLAYER_MAGIC, PLAYER_STORAGE_VERSION, bytes)?;
525 wincode::deserialize(&payload)
526 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
527}
528
529fn encode_global_file(file: &GlobalPlayerDataFile) -> io::Result<Vec<u8>> {
530 encode_file(
531 GLOBAL_MAGIC,
532 GLOBAL_STORAGE_VERSION,
533 wincode::serialize(file),
534 )
535}
536
537fn decode_global_file(bytes: &[u8]) -> io::Result<GlobalPlayerDataFile> {
538 let payload = decode_file(GLOBAL_MAGIC, GLOBAL_STORAGE_VERSION, bytes)?;
539 wincode::deserialize(&payload)
540 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
541}
542
543fn encode_file(
544 magic: [u8; 4],
545 version: u16,
546 serialized: wincode::WriteResult<Vec<u8>>,
547) -> io::Result<Vec<u8>> {
548 let payload =
549 serialized.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
550 let compressed = zstd::encode_all(&payload[..], 3)?;
551 let mut bytes = Vec::with_capacity(6 + compressed.len());
552 bytes.extend_from_slice(&magic);
553 bytes.extend_from_slice(&version.to_le_bytes());
554 bytes.extend_from_slice(&compressed);
555 Ok(bytes)
556}
557
558fn decode_file(
559 expected_magic: [u8; 4],
560 expected_version: u16,
561 bytes: &[u8],
562) -> io::Result<Vec<u8>> {
563 if bytes.len() < 6 {
564 return Err(io::Error::new(
565 io::ErrorKind::InvalidData,
566 "player data file is too short",
567 ));
568 }
569 if bytes[0..4] != expected_magic {
570 return Err(io::Error::new(
571 io::ErrorKind::InvalidData,
572 "invalid player data magic",
573 ));
574 }
575 let version = u16::from_le_bytes([bytes[4], bytes[5]]);
576 if version != expected_version {
577 return Err(io::Error::new(
578 io::ErrorKind::InvalidData,
579 format!("unsupported player data storage version {version}"),
580 ));
581 }
582 zstd::decode_all(&bytes[6..])
583}