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