Skip to main content

steel_core/player/game_mode/
player_methods.rs

1use super::{
2    AttributeModifier, AttributeModifierOperation, Axis, BlockCollisionContext, BlockPos,
3    CBlockChangedAck, CChangeDifficulty, CGameEvent, CPlayerInfoUpdate,
4    CREATIVE_BLOCK_RANGE_MODIFIER_AMOUNT, CREATIVE_ENTITY_RANGE_MODIFIER_AMOUNT, CSetCamera,
5    CollisionWorld, Difficulty, Entity, FLIGHT_DISABLE_RANGE, GameEventType, GameType, Identifier,
6    LivingEntity, Player, SSpectatorAction, WorldAabb, WorldCollisionProvider,
7    player_can_change_difficulty, shapes, vanilla_attributes,
8};
9use crate::behavior::blocks::PowderSnowBlock;
10use steel_protocol::packets::game::SPunch;
11use steel_utils::types::InteractionHand;
12
13impl Player {
14    /// Default reach distance for block interactions, in blocks.
15    pub const DEFAULT_BLOCK_INTERACTION_RANGE: f64 = 4.5;
16    /// Default reach distance for entity interactions, in blocks.
17    pub const DEFAULT_ENTITY_INTERACTION_RANGE: f64 = 3.0;
18
19    /// Sets the player's game mode and notifies the client.
20    ///
21    /// Returns `true` if the game mode was changed, `false` if the player was already in the requested game mode.
22    pub fn set_game_mode(&self, gamemode: GameType) -> bool {
23        let was_spectator = self.game_mode() == GameType::Spectator;
24        if !self.change_game_mode_state(gamemode) {
25            return false;
26        }
27
28        // Update abilities based on new game mode (mirrors vanilla GameType.updatePlayerAbilities)
29        let flying_after_update = {
30            let mut abilities = self.abilities.lock();
31            abilities.update_for_game_mode(gamemode);
32            abilities.flying
33        };
34        if flying_after_update
35            && gamemode != GameType::Spectator
36            && self.is_in_range_of_ground_for_flight_disable()
37        {
38            self.set_flying(false);
39        }
40        self.send_abilities();
41        self.update_game_mode_invisibility();
42
43        let update_packet =
44            CPlayerInfoUpdate::update_game_mode(self.gameprofile.id(), gamemode as i32);
45        self.server().broadcast_to_online(update_packet);
46
47        self.get_world().update_sleeping_player_list();
48
49        if gamemode == GameType::Creative {
50            self.reset_current_impulse_context();
51        }
52
53        self.send_packet(CGameEvent {
54            event: GameEventType::ChangeGameMode,
55            data: gamemode.into(),
56        });
57
58        if gamemode == GameType::Spectator {
59            self.stop_riding();
60            // TODO: Remove shoulder entities once player shoulder storage is implemented.
61            // TODO: Stop item use once living item-use state is implemented.
62            // TODO: Stop location-based enchantment effects once those effects are implemented.
63        } else if was_spectator {
64            self.send_packet(CSetCamera {
65                camera_id: self.id(),
66            });
67            // TODO: Restart location-based enchantment effects once those effects are implemented.
68        }
69
70        self.send_abilities();
71        self.update_game_mode_invisibility();
72        self.living_base.mark_effects_dirty();
73
74        true
75    }
76
77    fn update_game_mode_invisibility(&self) {
78        self.living_base.mark_effects_dirty();
79        self.update_dirty_mob_effect_entity_data();
80        self.sync_entity_data();
81    }
82
83    fn is_in_range_of_ground_for_flight_disable(&self) -> bool {
84        let world = self.get_world();
85        let collision_world = WorldCollisionProvider::for_entity(&world, self);
86        let bounding_box = self.bounding_box();
87        let collision_context =
88            BlockCollisionContext::entity(self.position().y, self.is_descending())
89                .with_fall_distance(self.fall_distance())
90                .with_can_walk_on_powder_snow(PowderSnowBlock::can_entity_walk_on_powder_snow(
91                    self,
92                ));
93
94        if collision_world.has_collision_with_context(&bounding_box, collision_context) {
95            return false;
96        }
97
98        let below = WorldAabb::new(
99            bounding_box.min_x(),
100            bounding_box.min_y() - FLIGHT_DISABLE_RANGE,
101            bounding_box.min_z(),
102            bounding_box.max_x(),
103            bounding_box.min_y(),
104            bounding_box.max_z(),
105        );
106        let colliders = collision_world.get_collisions_with_context(&below, collision_context);
107        if colliders.is_empty() {
108            return false;
109        }
110
111        let available_space_below =
112            -shapes::collide(Axis::Y, &bounding_box, &colliders, -FLIGHT_DISABLE_RANGE);
113        available_space_below < FLIGHT_DISABLE_RANGE
114    }
115
116    /// Sends the current world difficulty to the client.
117    pub fn send_difficulty(&self) {
118        let world = self.get_world();
119        let level_data = world.level_data.read();
120        let difficulty = level_data.data().difficulty;
121        let locked = level_data.data().difficulty_locked;
122        drop(level_data);
123        self.send_packet(CChangeDifficulty { difficulty, locked });
124    }
125
126    /// Handles a client request to change the world difficulty.
127    pub fn handle_change_difficulty(&self, difficulty: Difficulty) {
128        let world = self.get_world();
129        if !player_can_change_difficulty(self, &world) {
130            log::warn!(
131                "Player {} tried to change difficulty to {difficulty:?} without permission",
132                self.gameprofile.name()
133            );
134            return;
135        }
136        {
137            let level_data = world.level_data.read();
138            if level_data.data().difficulty_locked {
139                let current = level_data.data().difficulty;
140                drop(level_data);
141                self.send_packet(CChangeDifficulty {
142                    difficulty: current,
143                    locked: true,
144                });
145                return;
146            }
147        }
148
149        let domain = self.get_world().domain().to_owned();
150        for world in self.server().worlds.worlds_in_domain(&domain) {
151            world.set_difficulty(difficulty);
152        }
153    }
154
155    /// Updates interaction range attribute modifiers based on game mode.
156    ///
157    /// Vanilla: `ServerPlayer.updatePlayerAttributes()` — applies creative-mode
158    /// range modifiers every tick.
159    pub(in crate::player) fn update_player_attributes(&self) {
160        let is_creative = self.game_mode() == GameType::Creative;
161        let mut attrs = self.attributes().lock();
162
163        if is_creative {
164            attrs.set_modifier(
165                vanilla_attributes::BLOCK_INTERACTION_RANGE,
166                AttributeModifier {
167                    id: Identifier::vanilla_static("creative_mode_block_range"),
168                    amount: CREATIVE_BLOCK_RANGE_MODIFIER_AMOUNT,
169                    operation: AttributeModifierOperation::AddValue,
170                },
171                false,
172            );
173            attrs.set_modifier(
174                vanilla_attributes::ENTITY_INTERACTION_RANGE,
175                AttributeModifier {
176                    id: Identifier::vanilla_static("creative_mode_entity_range"),
177                    amount: CREATIVE_ENTITY_RANGE_MODIFIER_AMOUNT,
178                    operation: AttributeModifierOperation::AddValue,
179                },
180                false,
181            );
182        } else {
183            attrs.remove_modifier(
184                vanilla_attributes::BLOCK_INTERACTION_RANGE,
185                &Identifier::vanilla_static("creative_mode_block_range"),
186            );
187            attrs.remove_modifier(
188                vanilla_attributes::ENTITY_INTERACTION_RANGE,
189                &Identifier::vanilla_static("creative_mode_entity_range"),
190            );
191        }
192    }
193
194    /// Returns true if player has infinite materials (Creative mode).
195    #[must_use]
196    pub fn has_infinite_materials(&self) -> bool {
197        self.game_mode() == GameType::Creative
198    }
199
200    /// Acknowledges block changes up to the given sequence number.
201    ///
202    /// The ack is batched and sent once per tick (in `tick_ack_block_changes`),
203    /// matching vanilla behavior.
204    pub fn ack_block_changes_up_to(&self, sequence: i32) {
205        self.tick_state.lock().ack_block_changes_up_to(sequence);
206    }
207
208    /// Sends pending block change ack if any. Called once per tick.
209    pub(in crate::player) fn tick_ack_block_changes(&self) {
210        let sequence = self.tick_state.lock().take_ack_block_changes_up_to();
211        if sequence > -1 {
212            self.send_packet(CBlockChangedAck { sequence });
213        }
214    }
215
216    /// Returns true if player is within block interaction range.
217    ///
218    /// Uses eye position and AABB distance (nearest point on block surface),
219    /// matching vanilla's `Player.isWithinBlockInteractionRange(pos, 1.0)`.
220    #[must_use]
221    pub fn is_within_block_interaction_range(&self, pos: BlockPos) -> bool {
222        self.is_within_block_interaction_range_with_buffer(pos, 1.0)
223    }
224    /// Returns this player's current block interaction range, from the
225    /// attribute modifier if set, or the default range otherwise.
226    #[must_use]
227    pub fn block_interaction_range(&self) -> f64 {
228        self.attributes()
229            .lock()
230            .get_value(vanilla_attributes::BLOCK_INTERACTION_RANGE)
231            .unwrap_or(Self::DEFAULT_BLOCK_INTERACTION_RANGE)
232    }
233
234    /// Returns true if player is within block interaction range plus a vanilla buffer.
235    #[must_use]
236    pub fn is_within_block_interaction_range_with_buffer(
237        &self,
238        pos: BlockPos,
239        buffer: f64,
240    ) -> bool {
241        let player_pos = self.position();
242        let eye_y = player_pos.y + self.get_eye_height();
243
244        let min_x = f64::from(pos.x());
245        let min_y = f64::from(pos.y());
246        let min_z = f64::from(pos.z());
247        let max_x = min_x + 1.0;
248        let max_y = min_y + 1.0;
249        let max_z = min_z + 1.0;
250
251        let dx = f64::max(f64::max(min_x - player_pos.x, player_pos.x - max_x), 0.0);
252        let dy = f64::max(f64::max(min_y - eye_y, eye_y - max_y), 0.0);
253        let dz = f64::max(f64::max(min_z - player_pos.z, player_pos.z - max_z), 0.0);
254        let dist_sq = dx * dx + dy * dy + dz * dz;
255
256        let base_range = self.block_interaction_range();
257        let max_range = base_range + buffer;
258        dist_sq < max_range * max_range
259    }
260
261    /// Returns true if the player's eye position is within entity interaction range.
262    #[must_use]
263    pub fn is_within_entity_interaction_range(&self, aabb: WorldAabb, buffer: f64) -> bool {
264        let player_pos = self.position();
265        let eye_y = player_pos.y + self.get_eye_height();
266
267        let dx = f64::max(
268            f64::max(aabb.min_x() - player_pos.x, player_pos.x - aabb.max_x()),
269            0.0,
270        );
271        let dy = f64::max(f64::max(aabb.min_y() - eye_y, eye_y - aabb.max_y()), 0.0);
272        let dz = f64::max(
273            f64::max(aabb.min_z() - player_pos.z, player_pos.z - aabb.max_z()),
274            0.0,
275        );
276        let dist_sq = dx * dx + dy * dy + dz * dz;
277
278        let base_range = self
279            .attributes()
280            .lock()
281            .get_value(vanilla_attributes::ENTITY_INTERACTION_RANGE)
282            .unwrap_or(Self::DEFAULT_ENTITY_INTERACTION_RANGE);
283        let max_range = base_range + buffer;
284        dist_sq < max_range * max_range
285    }
286
287    /// Handles spectator camera selection.
288    pub fn handle_spectator_action(&self, packet: SSpectatorAction) {
289        if !self.has_client_loaded() || self.game_mode() != GameType::Spectator {
290            return;
291        }
292
293        self.reset_last_action_time();
294
295        let Some(entity_id) = packet.spectate_entity_id else {
296            return;
297        };
298
299        let world = self.get_world();
300        let Some(target) = world.get_accessible_entity_by_id(entity_id) else {
301            return;
302        };
303
304        // TODO: Store the camera entity and apply world-border/cross-world
305        // setCamera semantics once those foundations exist.
306        if self.is_within_entity_interaction_range(target.bounding_box(), 3.0)
307            && target.is_pickable()
308        {
309            self.send_packet(CSetCamera {
310                camera_id: target.id(),
311            });
312        }
313    }
314
315    /// Returns true if player is sneaking (secondary use active).
316    #[must_use]
317    pub fn is_secondary_use_active(&self) -> bool {
318        self.is_crouching()
319    }
320
321    /// Handles a player punch packet. The punch always swings the main hand
322    /// with the held item's attack animation.
323    pub fn handle_punch(&self, _packet: &SPunch) {
324        self.reset_last_action_time();
325        self.swing(
326            InteractionHand::MainHand,
327            self.attack_animation(InteractionHand::MainHand),
328            false,
329        );
330        self.reset_attack_strength_ticker();
331    }
332}