Skip to main content

yog_core/
query.rs

1//! Records returned by the world-query part of [`Server`](crate::Server)
2//! (ABI minor 1), and their wire form.
3//!
4//! The Java host answers these queries with one tab-separated line; the
5//! `decode` functions here are the single place that reads that line, so the
6//! runtime and the tests agree on it.
7
8use crate::BlockPos;
9
10/// What kind of entity something is, as a bit set ([`EntityInfo::flags`]).
11pub mod entity_flags {
12    /// A `LivingEntity` (mobs, players, armor stands).
13    pub const LIVING: u32 = 1;
14    /// A player.
15    pub const PLAYER: u32 = 1 << 1;
16    /// An `Animal` or `WaterAnimal`.
17    pub const ANIMAL: u32 = 1 << 2;
18    /// An `Enemy` (hostile mobs).
19    pub const MONSTER: u32 = 1 << 3;
20    /// A dropped item (`ItemEntity`).
21    pub const ITEM: u32 = 1 << 4;
22    /// A `Mob` (has AI).
23    pub const MOB: u32 = 1 << 5;
24    /// An armor stand.
25    pub const ARMOR_STAND: u32 = 1 << 6;
26    /// Alive (not removed, not dead).
27    pub const ALIVE: u32 = 1 << 7;
28    /// A spectating player.
29    pub const SPECTATOR: u32 = 1 << 8;
30    /// A part of a multi-part entity (ender dragon parts).
31    pub const PART: u32 = 1 << 9;
32    /// A villager.
33    pub const VILLAGER: u32 = 1 << 10;
34}
35
36/// A loaded entity, as [`Server::entity_info`](crate::Server::entity_info)
37/// sees it.
38#[derive(Debug, Clone, PartialEq)]
39pub struct EntityInfo {
40    /// Registry id of its type, e.g. `"minecraft:zombie"`.
41    pub type_id: String,
42    /// Dimension id, e.g. `"minecraft:overworld"`.
43    pub dimension: String,
44    /// Feet position.
45    pub pos: (f64, f64, f64),
46    /// Eye height above `pos`.
47    pub eye_height: f64,
48    /// Bounding box width and height.
49    pub width: f64,
50    pub height: f64,
51    /// [`entity_flags`] bits.
52    pub flags: u32,
53    /// Display name (custom name, player name, or the type's name).
54    pub name: String,
55}
56
57impl EntityInfo {
58    pub fn has(&self, flag: u32) -> bool {
59        self.flags & flag != 0
60    }
61
62    /// Eye position.
63    pub fn eye_pos(&self) -> (f64, f64, f64) {
64        (self.pos.0, self.pos.1 + self.eye_height, self.pos.2)
65    }
66
67    /// `type\tdim\tx\ty\tz\teye\twidth\theight\tflags\tname`.
68    pub fn decode(line: &str) -> Option<Self> {
69        let mut it = line.splitn(10, '\t');
70        let type_id = it.next()?.to_owned();
71        let dimension = it.next()?.to_owned();
72        let mut num = || it.next()?.parse::<f64>().ok();
73        let pos = (num()?, num()?, num()?);
74        let eye_height = num()?;
75        let width = num()?;
76        let height = num()?;
77        let flags = it.next()?.parse().ok()?;
78        let name = it.next().unwrap_or_default().to_owned();
79        Some(Self {
80            type_id,
81            dimension,
82            pos,
83            eye_height,
84            width,
85            height,
86            flags,
87            name,
88        })
89    }
90}
91
92/// The six block faces, in Minecraft's `Direction` order.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Face {
95    Down,
96    Up,
97    North,
98    South,
99    West,
100    East,
101}
102
103impl Face {
104    pub const ALL: [Face; 6] = [
105        Face::Down,
106        Face::Up,
107        Face::North,
108        Face::South,
109        Face::West,
110        Face::East,
111    ];
112
113    pub fn from_index(i: u8) -> Option<Self> {
114        Self::ALL.get(i as usize).copied()
115    }
116
117    pub fn index(self) -> u8 {
118        self as u8
119    }
120
121    /// Unit step out of the face.
122    pub fn normal(self) -> (i32, i32, i32) {
123        match self {
124            Face::Down => (0, -1, 0),
125            Face::Up => (0, 1, 0),
126            Face::North => (0, 0, -1),
127            Face::South => (0, 0, 1),
128            Face::West => (-1, 0, 0),
129            Face::East => (1, 0, 0),
130        }
131    }
132}
133
134/// A block a ray stopped at.
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct BlockHit {
137    pub pos: BlockPos,
138    /// The face the ray entered through.
139    pub face: Face,
140    /// The exact hit point (on the block's surface).
141    pub hit: (f64, f64, f64),
142}
143
144impl BlockHit {
145    /// `x\ty\tz\tface\thx\thy\thz`.
146    pub fn decode(line: &str) -> Option<Self> {
147        let mut it = line.split('\t');
148        let mut int = || it.next()?.parse::<i32>().ok();
149        let pos = BlockPos::new(int()?, int()?, int()?);
150        let face = Face::from_index(u8::try_from(int()?).ok()?)?;
151        let mut num = || it.next()?.parse::<f64>().ok();
152        let hit = (num()?, num()?, num()?);
153        Some(Self { pos, face, hit })
154    }
155}
156
157/// A block's properties, as [`Server::block_info`](crate::Server::block_info)
158/// sees them.
159#[derive(Debug, Clone, PartialEq)]
160pub struct BlockInfo {
161    /// Full block state, e.g. `"minecraft:oak_log[axis=y]"`.
162    pub state: String,
163    /// Destroy time; negative for unbreakable blocks (bedrock).
164    pub hardness: f32,
165    /// Placing a block here replaces this one (air, grass, water…).
166    pub replaceable: bool,
167    /// Air of any kind.
168    pub air: bool,
169    /// Id of the fluid in this block (`"minecraft:water"`), if any.
170    pub fluid: Option<String>,
171    /// Whether that fluid is a source block.
172    pub fluid_source: bool,
173    /// Redstone power the block receives (0–15).
174    pub redstone: u8,
175    /// Inside the world's build height and world border.
176    pub in_world: bool,
177}
178
179impl BlockInfo {
180    /// Registry id of the block, without its state properties.
181    pub fn block_id(&self) -> &str {
182        self.state.split('[').next().unwrap_or_default()
183    }
184
185    /// `state\thardness\treplaceable\tair\tfluid\tsource\tredstone\tin_world`
186    /// (booleans as `0`/`1`, an empty `fluid` for none).
187    pub fn decode(line: &str) -> Option<Self> {
188        let mut it = line.split('\t');
189        let state = it.next()?.to_owned();
190        let hardness = it.next()?.parse().ok()?;
191        let flag = |s: Option<&str>| s.map(|s| s == "1");
192        let replaceable = flag(it.next())?;
193        let air = flag(it.next())?;
194        let fluid = it.next()?;
195        let fluid = (!fluid.is_empty()).then(|| fluid.to_owned());
196        let fluid_source = flag(it.next())?;
197        let redstone = it.next()?.parse().ok()?;
198        let in_world = flag(it.next())?;
199        Some(Self {
200            state,
201            hardness,
202            replaceable,
203            air,
204            fluid,
205            fluid_source,
206            redstone,
207            in_world,
208        })
209    }
210}
211
212/// An online player's state, as
213/// [`Server::player_info`](crate::Server::player_info) sees it.
214#[derive(Debug, Clone, PartialEq)]
215pub struct PlayerInfo {
216    pub uuid: String,
217    /// `"survival"`, `"creative"`, `"adventure"` or `"spectator"`.
218    pub gamemode: String,
219    /// Selected hotbar slot (0–8).
220    pub selected_slot: u32,
221    pub xp_level: i32,
222    /// Total experience points.
223    pub xp_total: i32,
224    /// Air supply in ticks (300 when full).
225    pub air: i32,
226    /// Whether the player may fly (`abilities.mayfly`).
227    pub may_fly: bool,
228    pub flying: bool,
229}
230
231impl PlayerInfo {
232    pub fn is_creative(&self) -> bool {
233        self.gamemode == "creative"
234    }
235
236    /// `uuid\tgamemode\tslot\tlevel\ttotal\tair\tmay_fly\tflying`.
237    pub fn decode(line: &str) -> Option<Self> {
238        let mut it = line.split('\t');
239        let uuid = it.next()?.to_owned();
240        let gamemode = it.next()?.to_owned();
241        let selected_slot = it.next()?.parse().ok()?;
242        let xp_level = it.next()?.parse().ok()?;
243        let xp_total = it.next()?.parse().ok()?;
244        let air = it.next()?.parse().ok()?;
245        let may_fly = it.next()? == "1";
246        let flying = it.next()? == "1";
247        Some(Self {
248            uuid,
249            gamemode,
250            selected_slot,
251            xp_level,
252            xp_total,
253            air,
254            may_fly,
255            flying,
256        })
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn records_decode() {
266        let e = EntityInfo::decode(
267            "minecraft:zombie\tminecraft:overworld\t1.5\t64\t-2\t1.74\t0.6\t1.95\t169\tZom\tbie",
268        )
269        .unwrap();
270        assert_eq!(e.pos, (1.5, 64.0, -2.0));
271        assert!(e.has(entity_flags::LIVING) && e.has(entity_flags::MONSTER));
272        assert!(!e.has(entity_flags::PLAYER));
273        assert_eq!(e.name, "Zom\tbie");
274        assert_eq!(e.eye_pos().1, 65.74);
275
276        let h = BlockHit::decode("1\t2\t3\t5\t1.0\t2.5\t3.5").unwrap();
277        assert_eq!(h.face, Face::East);
278        assert_eq!(h.face.normal(), (1, 0, 0));
279
280        let b = BlockInfo::decode("minecraft:water[level=0]\t100\t1\t0\tminecraft:water\t1\t3\t1")
281            .unwrap();
282        assert_eq!(b.block_id(), "minecraft:water");
283        assert_eq!(b.fluid.as_deref(), Some("minecraft:water"));
284        assert!(b.fluid_source && b.replaceable && b.in_world && !b.air);
285        assert_eq!(b.redstone, 3);
286
287        let p = PlayerInfo::decode("u\tcreative\t4\t30\t1395\t300\t1\t0").unwrap();
288        assert!(p.is_creative() && p.may_fly && !p.flying);
289        assert_eq!(p.selected_slot, 4);
290
291        assert!(EntityInfo::decode("x\ty\tnope").is_none());
292        assert!(BlockHit::decode("1\t2\t3\t9\t0\t0\t0").is_none());
293    }
294}