use crate::BlockPos;
pub mod entity_flags {
pub const LIVING: u32 = 1;
pub const PLAYER: u32 = 1 << 1;
pub const ANIMAL: u32 = 1 << 2;
pub const MONSTER: u32 = 1 << 3;
pub const ITEM: u32 = 1 << 4;
pub const MOB: u32 = 1 << 5;
pub const ARMOR_STAND: u32 = 1 << 6;
pub const ALIVE: u32 = 1 << 7;
pub const SPECTATOR: u32 = 1 << 8;
pub const PART: u32 = 1 << 9;
pub const VILLAGER: u32 = 1 << 10;
}
#[derive(Debug, Clone, PartialEq)]
pub struct EntityInfo {
pub type_id: String,
pub dimension: String,
pub pos: (f64, f64, f64),
pub eye_height: f64,
pub width: f64,
pub height: f64,
pub flags: u32,
pub name: String,
}
impl EntityInfo {
pub fn has(&self, flag: u32) -> bool {
self.flags & flag != 0
}
pub fn eye_pos(&self) -> (f64, f64, f64) {
(self.pos.0, self.pos.1 + self.eye_height, self.pos.2)
}
pub fn decode(line: &str) -> Option<Self> {
let mut it = line.splitn(10, '\t');
let type_id = it.next()?.to_owned();
let dimension = it.next()?.to_owned();
let mut num = || it.next()?.parse::<f64>().ok();
let pos = (num()?, num()?, num()?);
let eye_height = num()?;
let width = num()?;
let height = num()?;
let flags = it.next()?.parse().ok()?;
let name = it.next().unwrap_or_default().to_owned();
Some(Self {
type_id,
dimension,
pos,
eye_height,
width,
height,
flags,
name,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Face {
Down,
Up,
North,
South,
West,
East,
}
impl Face {
pub const ALL: [Face; 6] = [
Face::Down,
Face::Up,
Face::North,
Face::South,
Face::West,
Face::East,
];
pub fn from_index(i: u8) -> Option<Self> {
Self::ALL.get(i as usize).copied()
}
pub fn index(self) -> u8 {
self as u8
}
pub fn normal(self) -> (i32, i32, i32) {
match self {
Face::Down => (0, -1, 0),
Face::Up => (0, 1, 0),
Face::North => (0, 0, -1),
Face::South => (0, 0, 1),
Face::West => (-1, 0, 0),
Face::East => (1, 0, 0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockHit {
pub pos: BlockPos,
pub face: Face,
pub hit: (f64, f64, f64),
}
impl BlockHit {
pub fn decode(line: &str) -> Option<Self> {
let mut it = line.split('\t');
let mut int = || it.next()?.parse::<i32>().ok();
let pos = BlockPos::new(int()?, int()?, int()?);
let face = Face::from_index(u8::try_from(int()?).ok()?)?;
let mut num = || it.next()?.parse::<f64>().ok();
let hit = (num()?, num()?, num()?);
Some(Self { pos, face, hit })
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BlockInfo {
pub state: String,
pub hardness: f32,
pub replaceable: bool,
pub air: bool,
pub fluid: Option<String>,
pub fluid_source: bool,
pub redstone: u8,
pub in_world: bool,
}
impl BlockInfo {
pub fn block_id(&self) -> &str {
self.state.split('[').next().unwrap_or_default()
}
pub fn decode(line: &str) -> Option<Self> {
let mut it = line.split('\t');
let state = it.next()?.to_owned();
let hardness = it.next()?.parse().ok()?;
let flag = |s: Option<&str>| s.map(|s| s == "1");
let replaceable = flag(it.next())?;
let air = flag(it.next())?;
let fluid = it.next()?;
let fluid = (!fluid.is_empty()).then(|| fluid.to_owned());
let fluid_source = flag(it.next())?;
let redstone = it.next()?.parse().ok()?;
let in_world = flag(it.next())?;
Some(Self {
state,
hardness,
replaceable,
air,
fluid,
fluid_source,
redstone,
in_world,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PlayerInfo {
pub uuid: String,
pub gamemode: String,
pub selected_slot: u32,
pub xp_level: i32,
pub xp_total: i32,
pub air: i32,
pub may_fly: bool,
pub flying: bool,
}
impl PlayerInfo {
pub fn is_creative(&self) -> bool {
self.gamemode == "creative"
}
pub fn decode(line: &str) -> Option<Self> {
let mut it = line.split('\t');
let uuid = it.next()?.to_owned();
let gamemode = it.next()?.to_owned();
let selected_slot = it.next()?.parse().ok()?;
let xp_level = it.next()?.parse().ok()?;
let xp_total = it.next()?.parse().ok()?;
let air = it.next()?.parse().ok()?;
let may_fly = it.next()? == "1";
let flying = it.next()? == "1";
Some(Self {
uuid,
gamemode,
selected_slot,
xp_level,
xp_total,
air,
may_fly,
flying,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn records_decode() {
let e = EntityInfo::decode(
"minecraft:zombie\tminecraft:overworld\t1.5\t64\t-2\t1.74\t0.6\t1.95\t169\tZom\tbie",
)
.unwrap();
assert_eq!(e.pos, (1.5, 64.0, -2.0));
assert!(e.has(entity_flags::LIVING) && e.has(entity_flags::MONSTER));
assert!(!e.has(entity_flags::PLAYER));
assert_eq!(e.name, "Zom\tbie");
assert_eq!(e.eye_pos().1, 65.74);
let h = BlockHit::decode("1\t2\t3\t5\t1.0\t2.5\t3.5").unwrap();
assert_eq!(h.face, Face::East);
assert_eq!(h.face.normal(), (1, 0, 0));
let b = BlockInfo::decode("minecraft:water[level=0]\t100\t1\t0\tminecraft:water\t1\t3\t1")
.unwrap();
assert_eq!(b.block_id(), "minecraft:water");
assert_eq!(b.fluid.as_deref(), Some("minecraft:water"));
assert!(b.fluid_source && b.replaceable && b.in_world && !b.air);
assert_eq!(b.redstone, 3);
let p = PlayerInfo::decode("u\tcreative\t4\t30\t1395\t300\t1\t0").unwrap();
assert!(p.is_creative() && p.may_fly && !p.flying);
assert_eq!(p.selected_slot, 4);
assert!(EntityInfo::decode("x\ty\tnope").is_none());
assert!(BlockHit::decode("1\t2\t3\t9\t0\t0\t0").is_none());
}
}