Skip to main content

steel_core/player/
player_data.rs

1//! Persistent player data structures.
2//!
3//! This module defines the data format for saving and loading player state.
4
5use rustc_hash::FxHashSet;
6use steel_registry::item_stack::ItemStack;
7use steel_registry::stat::Stat;
8use steel_utils::types::GameType;
9
10use super::{
11    Player, PlayerRespawnConfig, abilities::Abilities, experience::Experience, food_data::FoodData,
12    player_inventory::PlayerInventory,
13};
14use crate::player::stats_counter::StatState;
15use crate::{
16    chunk_saver::{ChunkStorage, PersistentEntity},
17    entity::{Entity, EntityFireFreezeState, LivingEntity},
18    inventory::{
19        container::Container,
20        ender_chest::{ENDER_CHEST_SLOTS, PlayerEnderChestContainer},
21    },
22};
23
24/// Current data version for player saves.
25/// Increment when making breaking changes to the format.
26pub const PLAYER_DATA_VERSION: i32 = 6;
27
28/// Persistent player data saved by Steel's storage backend.
29///
30/// This is Steel's runtime save snapshot. Vanilla import/export should live outside
31/// server runtime storage so compatibility logic does not constrain the native format.
32#[derive(Debug, Clone)]
33pub struct PersistentPlayerData {
34    /// Position (x, y, z) in absolute world coordinates.
35    pub pos: [f64; 3],
36
37    /// Velocity (x, y, z) in blocks per tick.
38    pub motion: [f64; 3],
39
40    /// Rotation (yaw, pitch) in degrees.
41    pub rotation: [f32; 2],
42
43    /// Whether the player is on the ground.
44    pub on_ground: bool,
45
46    /// Whether the player is elytra gliding.
47    pub fall_flying: bool,
48
49    /// Vanilla `remainingFireTicks`.
50    pub remaining_fire_ticks: i32,
51
52    /// Synchronized vanilla `TicksFrozen`.
53    pub ticks_frozen: i32,
54
55    /// Vanilla `isInPowderSnow`.
56    pub is_in_powder_snow: bool,
57
58    /// Vanilla `wasInPowderSnow`.
59    pub was_in_powder_snow: bool,
60
61    /// Vanilla `hasVisualFire`.
62    pub has_visual_fire: bool,
63
64    /// Current health points.
65    pub health: f32,
66
67    /// Current game mode (0=survival, 1=creative, 2=adventure, 3=spectator).
68    pub game_mode: i32,
69
70    /// Previous game mode of the player, or `None` if vanilla has not recorded one yet.
71    pub prev_game_mode: Option<i32>,
72
73    /// Player abilities (flight, invulnerability, etc.).
74    pub abilities: PersistentAbilities,
75
76    /// Inventory items with slot indices.
77    pub inventory: Vec<PersistentSlot>,
78
79    /// Currently selected hotbar slot (0-8).
80    pub selected_slot: i32,
81
82    /// Loaded world identifier (e.g., "minecraft:overworld").
83    pub world: String,
84
85    /// Current food level (0–20, default 20).
86    pub food_level: i32,
87
88    /// Food saturation level (0.0–`food_level`, default 5.0).
89    pub food_saturation_level: f32,
90
91    /// Accumulated food exhaustion (0.0–40.0, default 0.0).
92    pub food_exhaustion_level: f32,
93
94    /// Internal tick timer for regen/starvation (default 0).
95    pub food_tick_timer: i32,
96
97    /// Data version for format migrations.
98    pub data_version: i32,
99
100    /// Current experience level
101    pub experience_level: i32,
102
103    /// Vanilla progress toward the next experience level.
104    pub experience_progress: f32,
105
106    /// Vanilla `Player.totalExperience`, updated by point grants but independent of level/progress.
107    pub experience_total: i32,
108
109    /// Vanilla death-screen score. Point grants change it with Java `int` wrapping.
110    pub score: i32,
111
112    /// Vanilla `ServerPlayer.seenCredits`.
113    pub seen_credits: bool,
114
115    /// Vanilla one-player root vehicle tree stored with the player instead of chunk data.
116    pub root_vehicle: Option<PersistentRootVehicle>,
117
118    /// Vanilla per-player respawn configuration set by beds and respawn anchors.
119    pub respawn_config: Option<PlayerRespawnConfig>,
120
121    /// Vanilla in-flight ender pearls stored with the player (`ServerPlayer.enderPearls`).
122    pub ender_pearls: Vec<PersistentEnderPearl>,
123
124    /// Vanilla `EnderChest` items.
125    pub ender_items: Vec<PersistentSlot>,
126
127    /// The tracked statistics of the player with their counters.
128    pub stats: Vec<PersistentStat>,
129}
130
131/// A vanilla `RootVehicle` tree persisted with player data.
132#[derive(Debug, Clone)]
133pub struct PersistentRootVehicle {
134    /// UUID of the direct vehicle the player should reattach to.
135    pub attach: [u8; 16],
136    /// Root vehicle entity tree.
137    pub entity: PersistentEntity,
138}
139
140/// A thrown ender pearl persisted with its owning player.
141///
142/// Mirrors a vanilla `ender_pearls` list entry: the pearl entity plus the world
143/// it lives in (`ender_pearl_dimension`), so it re-spawns in its original world.
144#[derive(Debug, Clone)]
145pub struct PersistentEnderPearl {
146    /// Key of the world the pearl lives in.
147    pub world: String,
148    /// Serialized pearl entity.
149    pub entity: PersistentEntity,
150}
151
152/// Persistent abilities data.
153#[derive(Debug, Clone)]
154pub struct PersistentAbilities {
155    /// Whether the player is invulnerable to damage.
156    pub invulnerable: bool,
157    /// Whether the player is currently flying.
158    pub flying: bool,
159    /// Whether the player is allowed to fly.
160    pub may_fly: bool,
161    /// Whether the player can instantly break blocks (creative mode).
162    pub instabuild: bool,
163    /// Whether the player can place/break blocks.
164    pub may_build: bool,
165    /// Flying speed (default 0.05).
166    pub flying_speed: f32,
167    /// Walking speed (default 0.1).
168    pub walking_speed: f32,
169}
170
171/// An inventory slot with its index.
172#[derive(Debug, Clone)]
173pub struct PersistentSlot {
174    /// Slot index in the inventory.
175    pub slot: i8,
176    /// The item stack in this slot.
177    pub item: ItemStack,
178}
179
180/// Represents a tracked stat with its counter.
181#[derive(Debug, Clone)]
182pub struct PersistentStat {
183    /// The stat being tracked.
184    pub stat: Stat,
185    /// The current count of this stat.
186    pub count: i32,
187}
188
189impl PersistentPlayerData {
190    /// Extracts persistent data from a live player.
191    #[must_use]
192    pub fn from_player(player: &Player) -> Self {
193        let pos = player.position();
194        let (yaw, pitch) = player.rotation();
195        let delta = player.velocity();
196        let on_ground = player.on_ground();
197        let fall_flying = player.is_fall_flying();
198        let fire_freeze = player.fire_freeze_state();
199        let abilities = player.abilities.lock();
200        let inventory = player.inventory.lock();
201        let food_data = player.food_data.lock();
202
203        // Collect non-empty inventory slots
204        let mut slots = Vec::new();
205        // Main inventory (0-35) and equipment (36-42)
206        for slot in 0..PlayerInventory::CONTAINER_SIZE {
207            let item = inventory.get_item(slot);
208            if !item.is_empty() {
209                slots.push(PersistentSlot {
210                    slot: slot as i8,
211                    item: item.clone(),
212                });
213            }
214        }
215
216        let ender_chest_inventory = player.ender_chest_inventory.lock();
217        let mut ender_items = Vec::new();
218        for slot in 0..ENDER_CHEST_SLOTS {
219            let item = ender_chest_inventory.get_item(slot);
220            if !item.is_empty() {
221                ender_items.push(PersistentSlot {
222                    slot: slot as i8,
223                    item: item.clone(),
224                });
225            }
226        }
227
228        let (experience_level, experience_progress, experience_total) = {
229            let lock = player.experience.lock();
230            (lock.level(), lock.progress(), lock.total_points())
231        };
232        let score = player.score();
233        let root_vehicle = Self::root_vehicle_from_player(player)
234            .or_else(|| player.pending_root_vehicle_for_current_world());
235        let ender_pearls = Self::ender_pearls_from_player(player);
236        let stats = Self::stats_from_player(player);
237
238        Self {
239            pos: [pos.x, pos.y, pos.z],
240            motion: [delta.x, delta.y, delta.z],
241            rotation: [yaw, pitch],
242            on_ground,
243            fall_flying,
244            remaining_fire_ticks: fire_freeze.remaining_fire_ticks(),
245            ticks_frozen: fire_freeze.ticks_frozen(),
246            is_in_powder_snow: fire_freeze.is_in_powder_snow(),
247            was_in_powder_snow: fire_freeze.was_in_powder_snow(),
248            has_visual_fire: fire_freeze.has_visual_fire(),
249            health: player.get_health(),
250            game_mode: player.game_mode() as i32,
251            prev_game_mode: player
252                .previous_game_mode()
253                .map(|game_mode| game_mode as i32),
254            abilities: PersistentAbilities {
255                invulnerable: abilities.invulnerable,
256                flying: abilities.flying,
257                may_fly: abilities.may_fly,
258                instabuild: abilities.instabuild,
259                may_build: abilities.may_build,
260                flying_speed: abilities.flying_speed,
261                walking_speed: abilities.walking_speed,
262            },
263            inventory: slots,
264            selected_slot: i32::from(inventory.get_selected_slot()),
265            world: player.get_world().key.to_string(),
266            food_level: food_data.food_level,
267            food_saturation_level: food_data.saturation_level,
268            food_exhaustion_level: food_data.exhaustion_level,
269            food_tick_timer: food_data.tick_timer,
270            data_version: PLAYER_DATA_VERSION,
271            experience_level,
272            experience_progress,
273            experience_total,
274            score,
275            seen_credits: player.has_seen_credits(),
276            root_vehicle,
277            respawn_config: player.respawn_config(),
278
279            ender_pearls,
280            ender_items,
281            stats,
282        }
283    }
284
285    /// Snapshots the player's live in-flight ender pearls for persistence.
286    fn ender_pearls_from_player(player: &Player) -> Vec<PersistentEnderPearl> {
287        let mut seen = FxHashSet::default();
288        let mut pearls = player
289            .ender_pearls()
290            .iter()
291            .filter_map(|pearl| {
292                let world = pearl.level()?.key.to_string();
293                let entity = ChunkStorage::entity_tree_to_persistent(pearl)?;
294                seen.insert(entity.uuid);
295                Some(PersistentEnderPearl { world, entity })
296            })
297            .collect::<Vec<_>>();
298        pearls.extend(
299            player
300                .pending_ender_pearls()
301                .into_iter()
302                .filter(|pearl| seen.insert(pearl.entity.uuid)),
303        );
304        pearls
305    }
306
307    /// Snapshots the player's tracked stats and their counters for persistence.
308    fn stats_from_player(player: &Player) -> Vec<PersistentStat> {
309        player
310            .stats()
311            .into_iter()
312            .map(|(stat, count)| PersistentStat { stat, count })
313            .collect()
314    }
315
316    fn root_vehicle_from_player(player: &Player) -> Option<PersistentRootVehicle> {
317        let vehicle = player.vehicle()?;
318        let root_vehicle = player.root_vehicle()?;
319        if root_vehicle.id() == player.id() || !root_vehicle.has_exactly_one_player_passenger() {
320            return None;
321        }
322
323        let entity = ChunkStorage::entity_tree_to_persistent(&root_vehicle)?;
324        Some(PersistentRootVehicle {
325            attach: *vehicle.uuid().as_bytes(),
326            entity,
327        })
328    }
329}
330
331impl Player {
332    /// Resets domain-scoped gameplay data to the defaults used for a new player.
333    pub(crate) fn reset_domain_data_for_first_visit(&self) {
334        use glam::DVec3;
335
336        self.set_velocity(DVec3::ZERO);
337        self.set_on_ground(false);
338        self.set_fall_flying(false);
339        self.base()
340            .set_fire_freeze_state(EntityFireFreezeState::new());
341        self.sync_base_fire_freeze_entity_data();
342        self.set_health(self.get_max_health());
343        *self.abilities.lock() = Abilities::default();
344        *self.inventory.lock() = PlayerInventory::new();
345        *self.ender_chest_inventory.lock() = PlayerEnderChestContainer::new();
346        *self.food_data.lock() = FoodData::new();
347        self.stats.lock().reset();
348
349        let mut experience = Experience::default();
350        experience.dirty = true;
351        *self.experience.lock() = experience;
352
353        self.set_score(0);
354        self.set_seen_credits(false);
355    }
356}
357
358impl Default for PersistentAbilities {
359    fn default() -> Self {
360        Self {
361            invulnerable: false,
362            flying: false,
363            may_fly: false,
364            instabuild: false,
365            may_build: true,
366            flying_speed: 0.05,
367            walking_speed: 0.1,
368        }
369    }
370}
371
372impl From<&Abilities> for PersistentAbilities {
373    fn from(abilities: &Abilities) -> Self {
374        Self {
375            invulnerable: abilities.invulnerable,
376            flying: abilities.flying,
377            may_fly: abilities.may_fly,
378            instabuild: abilities.instabuild,
379            may_build: abilities.may_build,
380            flying_speed: abilities.flying_speed,
381            walking_speed: abilities.walking_speed,
382        }
383    }
384}
385
386impl From<PersistentAbilities> for Abilities {
387    fn from(persistent: PersistentAbilities) -> Self {
388        Self {
389            invulnerable: persistent.invulnerable,
390            flying: persistent.flying,
391            may_fly: persistent.may_fly,
392            instabuild: persistent.instabuild,
393            may_build: persistent.may_build,
394            flying_speed: persistent.flying_speed,
395            walking_speed: persistent.walking_speed,
396        }
397    }
398}
399
400impl PersistentPlayerData {
401    /// Applies the saved data to a player.
402    ///
403    /// This restores position, rotation, inventory, abilities, etc.
404    pub fn apply_to_player(&self, player: &Player) {
405        self.apply_to_player_inner(player, true);
406    }
407
408    /// Applies saved gameplay state without restoring world-local location data.
409    ///
410    /// Used when the saved world is unavailable or differs from an explicitly
411    /// selected world, which must use the target spawn instead.
412    pub fn apply_to_player_without_location(&self, player: &Player) {
413        self.apply_to_player_inner(player, false);
414    }
415
416    fn apply_to_player_inner(&self, player: &Player, restore_location: bool) {
417        use glam::DVec3;
418
419        if restore_location {
420            // Position
421            player
422                .base()
423                .set_position_local(DVec3::new(self.pos[0], self.pos[1], self.pos[2]));
424
425            // Rotation
426            player.set_rotation((self.rotation[0], self.rotation[1]));
427
428            // Motion/velocity
429            player.set_velocity(DVec3::new(self.motion[0], self.motion[1], self.motion[2]));
430
431            // Ground state
432            player.set_fall_flying(self.fall_flying);
433            player.set_on_ground(self.on_ground);
434        }
435
436        player
437            .base()
438            .set_fire_freeze_state(EntityFireFreezeState::from_parts(
439                self.remaining_fire_ticks,
440                self.ticks_frozen,
441                self.is_in_powder_snow,
442                self.was_in_powder_snow,
443                self.has_visual_fire,
444            ));
445        player.sync_base_fire_freeze_entity_data();
446        player.set_respawn_position(self.respawn_config.clone(), false);
447
448        // Health
449        player.set_health(self.health);
450
451        // Game mode
452        player.restore_game_modes(
453            self.game_mode.into(),
454            self.prev_game_mode.map(GameType::from),
455        );
456
457        // Abilities
458        *player.abilities.lock() = self.abilities.clone().into();
459
460        // Inventory
461        {
462            let mut inventory = player.inventory.lock();
463            // Clear existing inventory first
464            for slot in 0..PlayerInventory::CONTAINER_SIZE {
465                inventory.set_item(slot, ItemStack::empty());
466            }
467            // Restore saved items
468            for slot_data in &self.inventory {
469                let slot_index = slot_data.slot as usize;
470                if slot_index < PlayerInventory::CONTAINER_SIZE {
471                    inventory.set_item(slot_index, slot_data.item.clone());
472                }
473            }
474            // Restore selected slot
475            let selected = self.selected_slot.clamp(0, 8) as u8;
476            inventory.set_selected_slot(selected);
477        }
478
479        {
480            let mut ender_chest_inventory = player.ender_chest_inventory.lock();
481            for slot in 0..ENDER_CHEST_SLOTS {
482                ender_chest_inventory.set_item(slot, ItemStack::empty());
483            }
484            for slot_data in &self.ender_items {
485                let slot_index = slot_data.slot as usize;
486                if slot_index < ENDER_CHEST_SLOTS {
487                    ender_chest_inventory.set_item(slot_index, slot_data.item.clone());
488                }
489            }
490        }
491
492        // Food data
493        {
494            let mut food = player.food_data.lock();
495            food.food_level = self.food_level;
496            food.saturation_level = self.food_saturation_level;
497            food.exhaustion_level = self.food_exhaustion_level;
498            food.tick_timer = self.food_tick_timer;
499        }
500
501        {
502            let mut experience = player.experience.lock();
503            *experience = Experience::from_parts(
504                self.experience_level,
505                self.experience_progress,
506                self.experience_total,
507            );
508        }
509        player.set_score(self.score);
510        player.set_seen_credits(self.seen_credits);
511
512        // Statistics
513        {
514            let mut stats = player.stats.lock();
515
516            // This resets all counters to zero, but also marks them as non-persistent. However, they still need
517            // to be sent to the client the next time they are queried.
518            // This is important so that stale data from the previous domain doesn't stay in the client's cache.
519            // We then populate the counter with all the serialized stat counters from the new domain.
520            stats.reset();
521
522            for PersistentStat { stat, count } in &self.stats {
523                stats.stats.insert(*stat, (*count, StatState::Dirty));
524            }
525        }
526    }
527}