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