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