1use crate::BlockPos;
9
10pub mod entity_flags {
12 pub const LIVING: u32 = 1;
14 pub const PLAYER: u32 = 1 << 1;
16 pub const ANIMAL: u32 = 1 << 2;
18 pub const MONSTER: u32 = 1 << 3;
20 pub const ITEM: u32 = 1 << 4;
22 pub const MOB: u32 = 1 << 5;
24 pub const ARMOR_STAND: u32 = 1 << 6;
26 pub const ALIVE: u32 = 1 << 7;
28 pub const SPECTATOR: u32 = 1 << 8;
30 pub const PART: u32 = 1 << 9;
32 pub const VILLAGER: u32 = 1 << 10;
34}
35
36#[derive(Debug, Clone, PartialEq)]
39pub struct EntityInfo {
40 pub type_id: String,
42 pub dimension: String,
44 pub pos: (f64, f64, f64),
46 pub eye_height: f64,
48 pub width: f64,
50 pub height: f64,
51 pub flags: u32,
53 pub name: String,
55}
56
57impl EntityInfo {
58 pub fn has(&self, flag: u32) -> bool {
59 self.flags & flag != 0
60 }
61
62 pub fn eye_pos(&self) -> (f64, f64, f64) {
64 (self.pos.0, self.pos.1 + self.eye_height, self.pos.2)
65 }
66
67 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#[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 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#[derive(Debug, Clone, Copy, PartialEq)]
136pub struct BlockHit {
137 pub pos: BlockPos,
138 pub face: Face,
140 pub hit: (f64, f64, f64),
142}
143
144impl BlockHit {
145 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#[derive(Debug, Clone, PartialEq)]
160pub struct BlockInfo {
161 pub state: String,
163 pub hardness: f32,
165 pub replaceable: bool,
167 pub air: bool,
169 pub fluid: Option<String>,
171 pub fluid_source: bool,
173 pub redstone: u8,
175 pub in_world: bool,
177}
178
179impl BlockInfo {
180 pub fn block_id(&self) -> &str {
182 self.state.split('[').next().unwrap_or_default()
183 }
184
185 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#[derive(Debug, Clone, PartialEq)]
215pub struct PlayerInfo {
216 pub uuid: String,
217 pub gamemode: String,
219 pub selected_slot: u32,
221 pub xp_level: i32,
222 pub xp_total: i32,
224 pub air: i32,
226 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 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}