Skip to main content

steel_core/player/
sleep.rs

1use glam::DVec3;
2use steel_protocol::packets::game::{AnimateAction, CAnimate};
3use steel_registry::{
4    blocks::{block_state_ext::BlockStateExt as _, properties::BlockStateProperties},
5    dimension_type::BedRuleValue,
6    vanilla_custom_stats,
7};
8use steel_utils::{BlockPos, Direction};
9use text_components::{TextComponent, translation::TranslatedMessage};
10
11use super::sleep_state::SLEEP_DURATION;
12use super::{Player, PlayerRespawnConfig};
13use crate::{
14    entity::{Entity, LivingEntity as _},
15    level_data::RespawnData,
16    world::World,
17};
18
19const BED_INTERACTION_XZ_RANGE: f64 = 3.0;
20const BED_INTERACTION_Y_RANGE: f64 = 2.0;
21
22#[derive(Debug)]
23pub(crate) enum BedSleepingProblem {
24    OtherProblem,
25    Message(Box<TextComponent>),
26}
27
28impl BedSleepingProblem {
29    #[must_use]
30    pub(crate) fn message(&self) -> Option<&TextComponent> {
31        match self {
32            Self::Message(message) => Some(message.as_ref()),
33            Self::OtherProblem => None,
34        }
35    }
36}
37
38impl Player {
39    pub(super) fn bed_rule_value_allows_in_world(world: &World, value: BedRuleValue) -> bool {
40        match value {
41            BedRuleValue::Always => true,
42            BedRuleValue::WhenDark => world.is_dark_outside(),
43            BedRuleValue::Never => false,
44        }
45    }
46
47    pub(super) fn bed_rule_value_allows(&self, value: BedRuleValue) -> bool {
48        Self::bed_rule_value_allows_in_world(&self.get_world(), value)
49    }
50
51    fn bed_rule_problem_message(&self) -> Option<TextComponent> {
52        self.get_world()
53            .dimension_type
54            .bed_rule
55            .error_message_key
56            .as_ref()
57            .map(|key| {
58                TranslatedMessage {
59                    key: (*key).into(),
60                    fallback: None,
61                    args: None,
62                }
63                .component()
64            })
65    }
66
67    fn bed_sleep_problem(&self) -> BedSleepingProblem {
68        self.bed_rule_problem_message()
69            .map_or(BedSleepingProblem::OtherProblem, |message| {
70                BedSleepingProblem::Message(Box::new(message))
71            })
72    }
73
74    fn is_reachable_bed_block_from_position(player_pos: DVec3, bed_block_pos: BlockPos) -> bool {
75        let bed_center = DVec3::new(
76            f64::from(bed_block_pos.x()) + 0.5,
77            f64::from(bed_block_pos.y()),
78            f64::from(bed_block_pos.z()) + 0.5,
79        );
80        (player_pos.x - bed_center.x).abs() <= BED_INTERACTION_XZ_RANGE
81            && (player_pos.y - bed_center.y).abs() <= BED_INTERACTION_Y_RANGE
82            && (player_pos.z - bed_center.z).abs() <= BED_INTERACTION_XZ_RANGE
83    }
84
85    fn bed_in_range(&self, pos: BlockPos, direction: Direction) -> bool {
86        Self::bed_in_range_from_position(self.position(), pos, direction)
87    }
88
89    fn bed_in_range_from_position(player_pos: DVec3, pos: BlockPos, direction: Direction) -> bool {
90        Self::is_reachable_bed_block_from_position(player_pos, pos)
91            || Self::is_reachable_bed_block_from_position(
92                player_pos,
93                direction.opposite().relative(pos),
94            )
95    }
96
97    fn bed_blocked(&self, pos: BlockPos, direction: Direction) -> bool {
98        Self::bed_blocked_with_free_at(pos, direction, |pos| self.free_at(pos))
99    }
100
101    fn bed_blocked_with_free_at(
102        pos: BlockPos,
103        direction: Direction,
104        mut free_at: impl FnMut(BlockPos) -> bool,
105    ) -> bool {
106        let above = pos.above();
107        !free_at(above) || !free_at(direction.opposite().relative(above))
108    }
109
110    fn free_at(&self, pos: BlockPos) -> bool {
111        !self.get_world().get_block_state(pos).is_suffocating()
112    }
113
114    pub(crate) fn stop_sleep_in_bed(&self, forceful_wakeup: bool, update_level_list: bool) {
115        if self.is_sleeping() {
116            let packet = CAnimate::new(self.id(), AnimateAction::WakeUp);
117            self.get_world()
118                .broadcast_to_entity_trackers(self.id(), packet.clone(), None);
119            self.send_packet(packet);
120        }
121
122        self.default_stop_sleeping();
123        if update_level_list {
124            self.get_world().update_sleeping_player_list();
125        }
126        self.set_sleep_counter(if forceful_wakeup { 0 } else { SLEEP_DURATION });
127        let (yaw, pitch) = self.rotation();
128        if let Err(error) = self.teleport(self.position(), yaw, pitch) {
129            log::warn!(
130                "Failed to teleport player {} after waking up: {error}",
131                self.id()
132            );
133        }
134        self.sync_entity_data();
135    }
136
137    pub(crate) fn start_sleep_in_bed(&self, pos: BlockPos) -> Result<(), BedSleepingProblem> {
138        let world = self.get_world();
139        let direction = world
140            .get_block_state(pos)
141            .get_value(&BlockStateProperties::HORIZONTAL_FACING);
142        if self.is_sleeping() || !Entity::is_alive(self) {
143            return Err(BedSleepingProblem::OtherProblem);
144        }
145
146        let rule = &world.dimension_type.bed_rule;
147        let can_sleep = self.bed_rule_value_allows(rule.can_sleep);
148        let can_set_spawn = self.bed_rule_value_allows(rule.can_set_spawn);
149        if !can_set_spawn && !can_sleep {
150            return Err(self.bed_sleep_problem());
151        }
152        if !self.bed_in_range(pos, direction) {
153            return Err(BedSleepingProblem::Message(Box::new(
154                TranslatedMessage {
155                    key: "block.minecraft.bed.too_far_away".into(),
156                    fallback: None,
157                    args: None,
158                }
159                .component(),
160            )));
161        }
162        if self.bed_blocked(pos, direction) {
163            return Err(BedSleepingProblem::Message(Box::new(
164                TranslatedMessage {
165                    key: "block.minecraft.bed.obstructed".into(),
166                    fallback: None,
167                    args: None,
168                }
169                .component(),
170            )));
171        }
172
173        if can_set_spawn {
174            self.set_respawn_position(
175                Some(PlayerRespawnConfig::new(
176                    RespawnData::of(world.key.clone(), pos, self.rotation().0, self.rotation().1),
177                    false,
178                )),
179                true,
180            );
181        }
182        if !can_sleep {
183            return Err(self.bed_sleep_problem());
184        }
185
186        // TODO: Mirror vanilla Monster::isPreventingPlayerRest once Steel has
187        // the required Monster capability/class foundation.
188        self.set_sleep_counter(0);
189        if self.start_sleeping(pos).is_err() {
190            return Err(BedSleepingProblem::OtherProblem);
191        }
192        self.sync_entity_data();
193        self.award_custom_stat(&vanilla_custom_stats::SLEEP_IN_BED);
194        // TODO: trigger CriteriaTriggers.SLEPT_IN_BED once the foundation for advancements exist.
195        if !world.can_sleep_through_nights() {
196            self.send_overlay_message(
197                &TranslatedMessage {
198                    key: "sleep.not_possible".into(),
199                    fallback: None,
200                    args: None,
201                }
202                .component(),
203            );
204        }
205        world.update_sleeping_player_list();
206        Ok(())
207    }
208
209    /// Returns the player's current vanilla respawn configuration.
210    #[must_use]
211    pub fn respawn_config(&self) -> Option<PlayerRespawnConfig> {
212        self.respawn_config.lock().clone()
213    }
214
215    /// Sets the player's vanilla bed or respawn-anchor target.
216    pub fn set_respawn_position(
217        &self,
218        respawn_config: Option<PlayerRespawnConfig>,
219        show_message: bool,
220    ) {
221        let mut current = self.respawn_config.lock();
222        if show_message
223            && respawn_config
224                .as_ref()
225                .is_some_and(|config| !config.is_same_position(current.as_ref()))
226        {
227            self.send_message(
228                &TranslatedMessage {
229                    key: "block.minecraft.set_spawn".into(),
230                    fallback: None,
231                    args: None,
232                }
233                .component(),
234            );
235        }
236        *current = respawn_config;
237    }
238
239    /// Returns vanilla `Player.sleepCounter`.
240    #[must_use]
241    pub fn sleep_counter(&self) -> i32 {
242        self.sleep_state.lock().sleep_counter()
243    }
244
245    /// Returns whether this player has slept long enough for vanilla night skip.
246    #[must_use]
247    pub fn is_sleeping_long_enough(&self) -> bool {
248        self.is_sleeping() && self.sleep_counter() >= SLEEP_DURATION
249    }
250
251    fn set_sleep_counter(&self, sleep_counter: i32) {
252        self.sleep_state.lock().set_sleep_counter(sleep_counter);
253    }
254
255    pub(super) fn tick_sleep_counter(&self) {
256        self.sleep_state
257            .lock()
258            .tick_sleep_counter(self.is_sleeping());
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use glam::DVec3;
265    use steel_utils::{BlockPos, Direction};
266
267    use super::Player;
268
269    #[test]
270    fn sleep_admission_rejects_a_player_outside_vanilla_range() {
271        let bed_pos = BlockPos::new(0, 64, 0);
272        let direction = Direction::South;
273
274        assert!(Player::bed_in_range_from_position(
275            DVec3::new(3.5, 64.0, 0.5),
276            bed_pos,
277            direction,
278        ));
279        assert!(!Player::bed_in_range_from_position(
280            DVec3::new(4.0, 64.0, 0.5),
281            bed_pos,
282            direction,
283        ));
284    }
285
286    #[test]
287    fn sleep_admission_checks_both_bed_halves_for_obstruction() {
288        let bed_pos = BlockPos::new(0, 64, 0);
289        let direction = Direction::South;
290        let above_head = bed_pos.above();
291        let above_foot = direction.opposite().relative(above_head);
292
293        assert!(!Player::bed_blocked_with_free_at(
294            bed_pos,
295            direction,
296            |_| true,
297        ));
298        assert!(Player::bed_blocked_with_free_at(
299            bed_pos,
300            direction,
301            |pos| pos != above_head,
302        ));
303        assert!(Player::bed_blocked_with_free_at(
304            bed_pos,
305            direction,
306            |pos| pos != above_foot,
307        ));
308    }
309}