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 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 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 #[must_use]
251 pub fn respawn_config(&self) -> Option<PlayerRespawnConfig> {
252 self.respawn_config.lock().clone()
253 }
254
255 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 #[must_use]
281 pub fn sleep_counter(&self) -> i32 {
282 self.sleep_state.lock().sleep_counter()
283 }
284
285 #[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}