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