Skip to main content

yog_player/
lib.rs

1//! Player access for Yog mods.
2//!
3//! [`Player`] is a thin wrapper over [`yog_entity::Entity`]: it delegates all
4//! entity-level operations (teleport, position, health …) to that layer and
5//! adds the player-specific extras (inventory, networking) on top.
6
7use yog_core::Server;
8use yog_entity::Entity;
9
10/// A handle to one player, bound to a [`Server`].
11///
12/// Construct with [`Player::new`] when you only have the player name (most
13/// event callbacks), or with [`Player::with_uuid`] when you also have the UUID
14/// (e.g. from [`yog_command::CommandContext`]) — the latter unlocks the full
15/// entity-layer API.
16pub struct Player<'a> {
17    server: &'a dyn Server,
18    name: String,
19    uuid: Option<String>,
20}
21
22impl<'a> Player<'a> {
23    /// Bind to the player called `name`. Entity-level ops that require a UUID
24    /// will return `None`/`false`; use [`Player::with_uuid`] to unlock them.
25    pub fn new(server: &'a dyn Server, name: impl Into<String>) -> Self {
26        Self {
27            server,
28            name: name.into(),
29            uuid: None,
30        }
31    }
32
33    /// Bind to the player with both `name` and `uuid` (full functionality).
34    pub fn with_uuid(
35        server: &'a dyn Server,
36        name: impl Into<String>,
37        uuid: impl Into<String>,
38    ) -> Self {
39        Self {
40            server,
41            name: name.into(),
42            uuid: Some(uuid.into()),
43        }
44    }
45
46    pub fn name(&self) -> &str {
47        &self.name
48    }
49
50    pub fn uuid(&self) -> Option<&str> {
51        self.uuid.as_deref()
52    }
53
54    /// The underlying [`Entity`] handle, available when a UUID was provided.
55    pub fn entity(&self) -> Option<Entity<'_>> {
56        self.uuid.as_deref().map(|u| Entity::new(self.server, u))
57    }
58
59    // ── player-specific ops ─────────────────────────────────────────────────
60
61    /// Give `count` of `item_id` (e.g. `"minecraft:diamond"`).
62    pub fn give(&self, item_id: &str, count: u32) -> bool {
63        self.server.give_item(&self.name, item_id, count)
64    }
65
66    /// Send a raw-byte packet to this player on `channel` (server → client).
67    pub fn send_packet(&self, channel: &str, payload: &[u8]) -> bool {
68        self.server.send_to_player(&self.name, channel, payload)
69    }
70
71    // ── entity-level ops (delegated) ────────────────────────────────────────
72
73    /// Teleport to `(x, y, z)`. Uses the entity layer when a UUID is known;
74    /// falls back to the player-name primitive otherwise.
75    pub fn teleport(&self, x: f64, y: f64, z: f64) -> bool {
76        match &self.uuid {
77            Some(u) => self.server.entity_teleport(u, x, y, z),
78            None => self.server.teleport(&self.name, x, y, z),
79        }
80    }
81
82    /// Current position, or `None` if the entity isn't loaded / UUID unknown.
83    pub fn position(&self) -> Option<(f64, f64, f64)> {
84        self.entity()?.position()
85    }
86
87    /// Health, or `None` if UUID unknown.
88    pub fn health(&self) -> Option<f32> {
89        self.entity()?.health()
90    }
91
92    /// Set health; returns `false` if UUID unknown.
93    pub fn set_health(&self, health: f32) -> bool {
94        self.entity()
95            .is_some_and(|e: Entity<'_>| e.set_health(health))
96    }
97
98    /// Kill/remove this player entity; returns `false` if UUID unknown.
99    pub fn kill(&self) -> bool {
100        self.entity().is_some_and(|e: Entity<'_>| e.kill())
101    }
102
103    /// Send a title+subtitle screen to this player.
104    pub fn send_title(
105        &self,
106        title: &str,
107        subtitle: &str,
108        fadein: i32,
109        stay: i32,
110        fadeout: i32,
111    ) -> bool {
112        self.server
113            .send_title(&self.name, title, subtitle, fadein, stay, fadeout)
114    }
115
116    /// Send a message to the action-bar (above hotbar).
117    pub fn send_actionbar(&self, message: &str) -> bool {
118        self.server.send_actionbar(&self.name, message)
119    }
120
121    /// Disconnect this player with a reason message.
122    pub fn kick(&self, reason: &str) -> bool {
123        self.server.kick_player(&self.name, reason)
124    }
125
126    /// Change this player's game mode (`"survival"`, `"creative"`, `"adventure"`, `"spectator"`).
127    pub fn set_gamemode(&self, gamemode: &str) -> bool {
128        self.server.set_gamemode(&self.name, gamemode)
129    }
130
131    /// Play a sound at this player's position (audible to nearby players too).
132    pub fn play_sound(&self, sound_id: &str, volume: f32, pitch: f32) -> bool {
133        self.server
134            .play_sound_to_player(&self.name, sound_id, volume, pitch)
135    }
136
137    pub fn add_effect(
138        &self,
139        effect_id: &str,
140        duration_ticks: i32,
141        amplifier: u8,
142        show_particles: bool,
143    ) -> bool {
144        self.entity().is_some_and(|e: Entity<'_>| {
145            e.add_effect(effect_id, duration_ticks, amplifier, show_particles)
146        })
147    }
148
149    pub fn remove_effect(&self, effect_id: &str) -> bool {
150        self.entity()
151            .is_some_and(|e: Entity<'_>| e.remove_effect(effect_id))
152    }
153
154    pub fn clear_effects(&self) -> bool {
155        self.entity().is_some_and(|e: Entity<'_>| e.clear_effects())
156    }
157
158    // ── inventory ────────────────────────────────────────────────────────────
159
160    /// Main inventory + hotbar (slots 0–35), excluding armor and offhand.
161    /// Each entry: `(slot_index, item_id, count)`.
162    pub fn inventory(&self) -> Vec<(u32, String, u32)> {
163        self.server
164            .player_inventory(&self.name)
165            .into_iter()
166            .filter(|(slot, _, _)| *slot <= 35)
167            .collect()
168    }
169
170    /// Hotbar only (slots 0–8).
171    pub fn hotbar(&self) -> Vec<(u32, String, u32)> {
172        self.server
173            .player_inventory(&self.name)
174            .into_iter()
175            .filter(|(slot, _, _)| *slot <= 8)
176            .collect()
177    }
178
179    /// Armor slots (36=boots, 37=leggings, 38=chestplate, 39=helmet).
180    /// Returned slot indices are remapped to 0–3.
181    pub fn armor(&self) -> Vec<(u32, String, u32)> {
182        self.server
183            .player_inventory(&self.name)
184            .into_iter()
185            .filter(|(slot, _, _)| *slot >= 36 && *slot <= 39)
186            .map(|(slot, id, count)| (slot - 36, id, count))
187            .collect()
188    }
189
190    /// Set or clear (count==0) a specific inventory slot.
191    pub fn set_slot(&self, slot: u32, item_id: &str, count: u32) -> bool {
192        self.server
193            .player_set_slot(&self.name, slot, item_id, count)
194    }
195
196    // ── cross-dimension teleport ─────────────────────────────────────────────
197
198    /// Teleport to `(x, y, z)` in `dimension` (may be a different dimension).
199    pub fn teleport_to_dim(&self, dimension: &str, x: f64, y: f64, z: f64) -> bool {
200        self.server.teleport_to_dim(&self.name, dimension, x, y, z)
201    }
202}