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