Sign in Sign up
kretrod/lodestone Public
Branches
master
408 lines (378 loc) · 15.6 KB Raw
//! Finding blocks worth seeing.
//!
//! Two very different problems, so two mechanisms:
//!
//! * **Ores** are ordinary blocks with no index anywhere, so they have to be
//!   read one position at a time. Far too slow to do in one frame for any
//!   useful radius, so the volume is swept a slice per frame on a fixed budget
//!   and the finished pass swapped in.
//! * **Containers, shulkers, ender chests, beacons** are *block entities*, and
//!   the client already keeps a map of those per chunk. Walking the loaded
//!   chunks reads every one of them across the whole render distance in a
//!   single pass — no sweeping, no budget, no waiting.
//!
//! Identity for ores: `Block` objects are singletons that do not override
//! `hashCode`, so `System.identityHashCode` is a stable key — one int compare
//! per position instead of a chain of reference tests.

use std::collections::HashMap;

use jni_sys::{jclass, jmethodID, jobject, jvalue};

use crate::jni::Jni;
use crate::state::{BaseHit, BlockKind};

/// Ore groups offered in the X-Ray list. One entry can cover several blocks,
/// because nobody wants to tick "diamond" and "deepslate diamond" separately.
pub const ORE_GROUPS: &[(&str, &[&str])] = &[
    ("Diamond", &["DIAMOND_ORE", "DEEPSLATE_DIAMOND_ORE"]),
    ("Ancient Debris", &["ANCIENT_DEBRIS"]),
    ("Emerald", &["EMERALD_ORE", "DEEPSLATE_EMERALD_ORE"]),
    ("Gold", &["GOLD_ORE", "DEEPSLATE_GOLD_ORE", "NETHER_GOLD_ORE"]),
    ("Iron", &["IRON_ORE", "DEEPSLATE_IRON_ORE"]),
    ("Redstone", &["REDSTONE_ORE", "DEEPSLATE_REDSTONE_ORE"]),
    ("Lapis", &["LAPIS_ORE", "DEEPSLATE_LAPIS_ORE"]),
    ("Copper", &["COPPER_ORE", "DEEPSLATE_COPPER_ORE"]),
    ("Coal", &["COAL_ORE", "DEEPSLATE_COAL_ORE"]),
    ("Quartz", &["NETHER_QUARTZ_ORE"]),
    ("Spawner", &["SPAWNER", "TRIAL_SPAWNER"]),
    ("Portal", &["END_PORTAL_FRAME", "NETHER_PORTAL"]),
    ("Chest", &["CHEST", "TRAPPED_CHEST", "BARREL"]),
];

/// Block-entity classes, and what they mean. The ones near the top are what
/// actually give a player base away.
const BLOCK_ENTITIES: &[(&str, &str, bool)] = &[
    // class, label, counts as a base marker
    ("net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity", "Shulker Box", true),
    ("net/minecraft/world/level/block/entity/EnderChestBlockEntity", "Ender Chest", true),
    ("net/minecraft/world/level/block/entity/BeaconBlockEntity", "Beacon", true),
    ("net/minecraft/world/level/block/entity/BrewingStandBlockEntity", "Brewing Stand", true),
    ("net/minecraft/world/level/block/entity/EnchantingTableBlockEntity", "Enchanting Table", true),
    ("net/minecraft/world/level/block/entity/ChestBlockEntity", "Chest", false),
    ("net/minecraft/world/level/block/entity/BarrelBlockEntity", "Barrel", false),
    ("net/minecraft/world/level/block/entity/HopperBlockEntity", "Hopper", false),
    ("net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity", "Furnace", false),
];

pub struct Blocks {
    block_pos_class: jclass,
    m_block_pos_init: jmethodID,
    m_get_block_state: jmethodID,
    m_get_block: jmethodID,
    m_blocks_motion: Option<jmethodID>,
    m_identity_hash: jmethodID,
    system_class: jclass,

    /// identityHashCode -> which ore group it belongs to.
    ore_kinds: HashMap<i32, usize>,

    // Block entities.
    m_get_chunk: Option<jmethodID>,
    m_chunk_block_entities: Option<jmethodID>,
    m_map_values: Option<jmethodID>,
    m_iterator: Option<jmethodID>,
    m_has_next: Option<jmethodID>,
    m_next: Option<jmethodID>,
    m_get_block_pos: Option<jmethodID>,
    m_pos_x: Option<jmethodID>,
    m_pos_y: Option<jmethodID>,
    m_pos_z: Option<jmethodID>,
    entity_classes: Vec<(jclass, &'static str, bool)>,

    // Ore sweep progress.
    origin: (i32, i32, i32),
    cursor: usize,
    radius: i32,
    partial: Vec<(i32, i32, i32, BlockKind)>,
    pub ores: Vec<(i32, i32, i32, BlockKind)>,
    /// Block entities from the most recent pass — rebuilt whole, every time.
    pub entities: Vec<BaseHit>,
}

impl Blocks {
    pub fn resolve(j: &Jni, missing: &mut Vec<String>) -> Option<Blocks> {
        let block_pos_class = global_class(j, "net/minecraft/core/BlockPos")?;
        let level = global_class(j, "net/minecraft/world/level/Level")?;
        let level_reader = global_class(j, "net/minecraft/world/level/LevelReader");
        let level_chunk = global_class(j, "net/minecraft/world/level/chunk/LevelChunk");
        let block_entity = global_class(j, "net/minecraft/world/level/block/entity/BlockEntity");
        let vec3i = global_class(j, "net/minecraft/core/Vec3i");
        let map = global_class(j, "java/util/Map");
        let collection = global_class(j, "java/util/Collection");
        let iterator = global_class(j, "java/util/Iterator");
        let state_base = global_class(
            j,
            "net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase",
        )?;
        let system_class = global_class(j, "java/lang/System")?;
        let blocks_class = global_class(j, "net/minecraft/world/level/block/Blocks")?;

        let m_block_pos_init = need(j.method(block_pos_class, "<init>", "(III)V"), "BlockPos.<init>(III)", missing)?;
        let m_get_block_state = need(
            j.method(
                level,
                "getBlockState",
                "(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;",
            ),
            "Level.getBlockState(BlockPos)",
            missing,
        )?;
        let m_get_block = need(
            j.method(state_base, "getBlock", "()Lnet/minecraft/world/level/block/Block;"),
            "BlockStateBase.getBlock()",
            missing,
        )?;
        let m_blocks_motion = j.method(state_base, "blocksMotion", "()Z");
        let m_identity_hash = need(
            j.static_method(system_class, "identityHashCode", "(Ljava/lang/Object;)I"),
            "System.identityHashCode(Object)",
            missing,
        )?;

        // Every ore group's blocks, keyed by identity.
        let mut ore_kinds = HashMap::new();
        for (index, (_, names)) in ORE_GROUPS.iter().enumerate() {
            for name in *names {
                let Some(field) =
                    j.static_field(blocks_class, name, "Lnet/minecraft/world/level/block/Block;")
                else {
                    continue;
                };
                let Some(block) = j.static_obj_field(blocks_class, field) else {
                    continue;
                };
                if let Some(hash) =
                    j.call_static_int(system_class, m_identity_hash, &[jvalue { l: block }])
                {
                    ore_kinds.insert(hash, index);
                }
                j.delete_local(block);
            }
        }

        let entity_classes = BLOCK_ENTITIES
            .iter()
            .filter_map(|(path, label, base)| {
                global_class(j, path).map(|c| (c, *label, *base))
            })
            .collect();

        Some(Blocks {
            block_pos_class,
            m_block_pos_init,
            m_get_block_state,
            m_get_block,
            m_blocks_motion,
            m_identity_hash,
            system_class,
            ore_kinds,
            m_get_chunk: level_reader.and_then(|c| {
                j.method(c, "getChunk", "(II)Lnet/minecraft/world/level/chunk/ChunkAccess;")
            }),
            m_chunk_block_entities: level_chunk
                .and_then(|c| j.method(c, "getBlockEntities", "()Ljava/util/Map;")),
            m_map_values: map.and_then(|c| j.method(c, "values", "()Ljava/util/Collection;")),
            m_iterator: collection
                .and_then(|c| j.method(c, "iterator", "()Ljava/util/Iterator;")),
            m_has_next: iterator.and_then(|c| j.method(c, "hasNext", "()Z")),
            m_next: iterator.and_then(|c| j.method(c, "next", "()Ljava/lang/Object;")),
            m_get_block_pos: block_entity
                .and_then(|c| j.method(c, "getBlockPos", "()Lnet/minecraft/core/BlockPos;")),
            m_pos_x: vec3i.and_then(|c| j.method(c, "getX", "()I")),
            m_pos_y: vec3i.and_then(|c| j.method(c, "getY", "()I")),
            m_pos_z: vec3i.and_then(|c| j.method(c, "getZ", "()I")),
            entity_classes,
            origin: (0, 0, 0),
            cursor: 0,
            radius: 0,
            partial: Vec::new(),
            ores: Vec::new(),
            entities: Vec::new(),
        })
    }

    /// Would this block stop you walking into it? `blocksMotion` is the game's
    /// own answer, so tall grass and water read as passable while a slab or a
    /// fence does not.
    pub fn blocks_motion(&self, j: &Jni, level: jobject, x: i32, y: i32, z: i32) -> bool {
        let Some(m) = self.m_blocks_motion else {
            return false;
        };
        let Some(pos) = j.new_object(
            self.block_pos_class,
            self.m_block_pos_init,
            &[jvalue { i: x }, jvalue { i: y }, jvalue { i: z }],
        ) else {
            return false;
        };
        let state = j.call_obj(level, self.m_get_block_state, &[jvalue { l: pos }]);
        j.delete_local(pos);
        let Some(state) = state else {
            return false;
        };
        let blocked = j.call_bool(state, m, &[]).unwrap_or(false);
        j.delete_local(state);
        blocked
    }

    // ---- block entities: one cheap pass over the loaded chunks ------------

    /// Read every block entity in the loaded chunks around the player. This is
    /// what container ESP and the base finder both run on.
    pub fn scan_block_entities(
        &mut self,
        j: &Jni,
        level: jobject,
        player: (f64, f64, f64),
        chunk_radius: i32,
        base_only: bool,
    ) {
        self.entities.clear();
        let (Some(get_chunk), Some(block_entities), Some(values), Some(iter), Some(has_next), Some(next)) = (
            self.m_get_chunk,
            self.m_chunk_block_entities,
            self.m_map_values,
            self.m_iterator,
            self.m_has_next,
            self.m_next,
        ) else {
            return;
        };

        let centre = (
            (player.0.floor() as i32) >> 4,
            (player.2.floor() as i32) >> 4,
        );
        for cx in (centre.0 - chunk_radius)..=(centre.0 + chunk_radius) {
            for cz in (centre.1 - chunk_radius)..=(centre.1 + chunk_radius) {
                let Some(chunk) =
                    j.call_obj(level, get_chunk, &[jvalue { i: cx }, jvalue { i: cz }])
                else {
                    continue;
                };
                let map = j.call_obj(chunk, block_entities, &[]);
                j.delete_local(chunk);
                let Some(map) = map else { continue };
                let collection = j.call_obj(map, values, &[]);
                j.delete_local(map);
                let Some(collection) = collection else { continue };
                let it = j.call_obj(collection, iter, &[]);
                j.delete_local(collection);
                let Some(it) = it else { continue };

                let mut guard = 0;
                while guard < 4096 {
                    guard += 1;
                    match j.call_bool(it, has_next, &[]) {
                        Some(true) => {}
                        _ => break,
                    }
                    let Some(be) = j.call_obj(it, next, &[]) else { break };
                    self.record(j, be, player, base_only);
                    j.delete_local(be);
                }
                j.delete_local(it);
            }
        }
        self.entities
            .sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
    }

    fn record(&mut self, j: &Jni, be: jobject, player: (f64, f64, f64), base_only: bool) {
        let Some((label, is_base)) = self.classify(j, be) else {
            return;
        };
        if base_only && !is_base {
            return;
        }
        let (Some(get_pos), Some(gx), Some(gy), Some(gz)) =
            (self.m_get_block_pos, self.m_pos_x, self.m_pos_y, self.m_pos_z)
        else {
            return;
        };
        let Some(pos) = j.call_obj(be, get_pos, &[]) else {
            return;
        };
        let x = j.call_int(pos, gx, &[]).unwrap_or(0);
        let y = j.call_int(pos, gy, &[]).unwrap_or(0);
        let z = j.call_int(pos, gz, &[]).unwrap_or(0);
        j.delete_local(pos);

        let dx = x as f64 + 0.5 - player.0;
        let dy = y as f64 + 0.5 - player.1;
        let dz = z as f64 + 0.5 - player.2;
        self.entities.push(BaseHit {
            x,
            y,
            z,
            label,
            is_base,
            distance: (dx * dx + dy * dy + dz * dz).sqrt() as f32,
        });
    }

    fn classify(&self, j: &Jni, be: jobject) -> Option<(&'static str, bool)> {
        for (class, label, is_base) in &self.entity_classes {
            if j.is_instance(be, *class) {
                return Some((label, *is_base));
            }
        }
        None
    }

    // ---- ores: a slice of the volume per frame ----------------------------

    pub fn step_ores(
        &mut self,
        j: &Jni,
        level: jobject,
        player_pos: (f64, f64, f64),
        radius: i32,
        selected: &[bool],
        budget: usize,
    ) {
        if !selected.iter().any(|s| *s) {
            self.ores.clear();
            self.partial.clear();
            self.cursor = 0;
            return;
        }

        let here = (
            player_pos.0.floor() as i32,
            player_pos.1.floor() as i32,
            player_pos.2.floor() as i32,
        );
        let moved = (here.0 - self.origin.0).abs()
            + (here.1 - self.origin.1).abs()
            + (here.2 - self.origin.2).abs();
        let side = (radius * 2 + 1) as usize;
        let total = side * side * side;

        if self.cursor >= total || moved > radius / 2 || self.radius != radius {
            if self.cursor >= total {
                std::mem::swap(&mut self.ores, &mut self.partial);
            }
            self.partial.clear();
            self.cursor = 0;
            self.origin = here;
            self.radius = radius;
        }

        let side_i = side as i32;
        let end = (self.cursor + budget).min(total);
        for index in self.cursor..end {
            let i = index as i32;
            let x = self.origin.0 - radius + (i % side_i);
            let y = self.origin.1 - radius + ((i / side_i) % side_i);
            let z = self.origin.2 - radius + (i / (side_i * side_i));
            if let Some(group) = self.group_at(j, level, x, y, z) {
                if selected.get(group).copied().unwrap_or(false) {
                    self.partial.push((x, y, z, BlockKind::Ore(group)));
                }
            }
        }
        self.cursor = end;
    }

    fn group_at(&self, j: &Jni, level: jobject, x: i32, y: i32, z: i32) -> Option<usize> {
        let pos = j.new_object(
            self.block_pos_class,
            self.m_block_pos_init,
            &[jvalue { i: x }, jvalue { i: y }, jvalue { i: z }],
        )?;
        let state = j.call_obj(level, self.m_get_block_state, &[jvalue { l: pos }]);
        j.delete_local(pos);
        let state = state?;
        let block = j.call_obj(state, self.m_get_block, &[]);
        j.delete_local(state);
        let block = block?;
        let hash =
            j.call_static_int(self.system_class, self.m_identity_hash, &[jvalue { l: block }]);
        j.delete_local(block);
        self.ore_kinds.get(&hash?).copied()
    }
}

fn need<T>(value: Option<T>, what: &str, missing: &mut Vec<String>) -> Option<T> {
    if value.is_none() {
        missing.push(what.to_string());
    }
    value
}

fn global_class(j: &Jni, name: &str) -> Option<jclass> {
    let local = j.find_class(name)?;
    j.global(local).map(|g| g as jclass)
}