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 pub const SNEAKING: u32 = 1 << 11;
36 pub const ON_GROUND: u32 = 1 << 12;
38 pub const HORIZONTAL_COLLISION: u32 = 1 << 13;
40 pub const GLIDING: u32 = 1 << 14;
42}
43
44#[derive(Debug, Clone, PartialEq)]
47pub struct EntityInfo {
48 pub type_id: String,
50 pub dimension: String,
52 pub pos: (f64, f64, f64),
54 pub eye_height: f64,
56 pub width: f64,
58 pub height: f64,
59 pub flags: u32,
61 pub name: String,
63}
64
65impl EntityInfo {
66 pub fn has(&self, flag: u32) -> bool {
67 self.flags & flag != 0
68 }
69
70 pub fn eye_pos(&self) -> (f64, f64, f64) {
72 (self.pos.0, self.pos.1 + self.eye_height, self.pos.2)
73 }
74
75 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#[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 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#[derive(Debug, Clone, Copy, PartialEq)]
144pub struct BlockHit {
145 pub pos: BlockPos,
146 pub face: Face,
148 pub hit: (f64, f64, f64),
150}
151
152impl BlockHit {
153 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#[derive(Debug, Clone, PartialEq)]
168pub struct BlockInfo {
169 pub state: String,
171 pub hardness: f32,
173 pub replaceable: bool,
175 pub air: bool,
177 pub fluid: Option<String>,
179 pub fluid_source: bool,
181 pub redstone: u8,
183 pub in_world: bool,
185}
186
187impl BlockInfo {
188 pub fn block_id(&self) -> &str {
190 self.state.split('[').next().unwrap_or_default()
191 }
192
193 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#[derive(Debug, Clone, PartialEq)]
223pub struct PlayerInfo {
224 pub uuid: String,
225 pub gamemode: String,
227 pub selected_slot: u32,
229 pub xp_level: i32,
230 pub xp_total: i32,
232 pub air: i32,
234 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 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}