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 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 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 #[must_use]
211 pub fn respawn_config(&self) -> Option<PlayerRespawnConfig> {
212 self.respawn_config.lock().clone()
213 }
214
215 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 #[must_use]
241 pub fn sleep_counter(&self) -> i32 {
242 self.sleep_state.lock().sleep_counter()
243 }
244
245 #[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}