Sign in Sign up
kretrod/lodestone Public
Branches
master
240 lines (222 loc) · 7.9 KB Raw
//! Block scanning: X-Ray and container highlighting.
//!
//! Entity ESP can walk a list the game already keeps. Blocks have no such list,
//! so this reads them one position at a time — which is far too slow to do in a
//! single frame for any useful radius. Instead the volume is walked a slice at
//! a time with a fixed budget per frame, and the completed result is swapped in
//! when a pass finishes. A pass restarts when the player has moved far enough
//! that the old result is stale.
//!
//! Identity: `Block` objects are singletons and do not override `hashCode`, so
//! `System.identityHashCode` is a stable key for "which block is this" — one
//! int to compare instead of a chain of reference comparisons.

use std::collections::HashMap;

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

use crate::jni::Jni;
use crate::state::BlockKind;

/// Ores worth seeing through stone.
const ORES: &[&str] = &[
    "COAL_ORE",
    "DEEPSLATE_COAL_ORE",
    "IRON_ORE",
    "DEEPSLATE_IRON_ORE",
    "COPPER_ORE",
    "DEEPSLATE_COPPER_ORE",
    "GOLD_ORE",
    "DEEPSLATE_GOLD_ORE",
    "REDSTONE_ORE",
    "DEEPSLATE_REDSTONE_ORE",
    "LAPIS_ORE",
    "DEEPSLATE_LAPIS_ORE",
    "EMERALD_ORE",
    "DEEPSLATE_EMERALD_ORE",
    "DIAMOND_ORE",
    "DEEPSLATE_DIAMOND_ORE",
    "NETHER_GOLD_ORE",
    "NETHER_QUARTZ_ORE",
    "ANCIENT_DEBRIS",
];

/// Things that hold loot.
const CONTAINERS: &[&str] = &[
    "CHEST",
    "TRAPPED_CHEST",
    "ENDER_CHEST",
    "BARREL",
    "SHULKER_BOX",
    "FURNACE",
    "BLAST_FURNACE",
    "SMOKER",
    "HOPPER",
    "DISPENSER",
    "DROPPER",
    "BREWING_STAND",
    "BEACON",
];

pub struct Blocks {
    block_pos_class: jclass,
    m_block_pos_init: jmethodID,
    m_get_block_state: jmethodID,
    m_get_block: jmethodID,
    m_identity_hash: jmethodID,
    system_class: jclass,

    /// identityHashCode -> what kind of thing it is.
    kinds: HashMap<i32, BlockKind>,

    /// Where the current pass started, and how far it has got.
    origin: (i32, i32, i32),
    cursor: usize,
    radius: i32,
    partial: Vec<(i32, i32, i32, BlockKind)>,
    /// The last completed pass, which is what gets drawn.
    pub found: Vec<(i32, i32, i32, BlockKind)>,
}

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 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 = j.method(block_pos_class, "<init>", "(III)V").or_else(|| {
            missing.push("BlockPos.<init>(III)".into());
            None
        })?;
        let m_get_block_state = j
            .method(
                level,
                "getBlockState",
                "(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;",
            )
            .or_else(|| {
                missing.push("Level.getBlockState(BlockPos)".into());
                None
            })?;
        let m_get_block = j
            .method(state_base, "getBlock", "()Lnet/minecraft/world/level/block/Block;")
            .or_else(|| {
                missing.push("BlockStateBase.getBlock()".into());
                None
            })?;
        let m_identity_hash = j
            .static_method(system_class, "identityHashCode", "(Ljava/lang/Object;)I")
            .or_else(|| {
                missing.push("System.identityHashCode(Object)".into());
                None
            })?;

        // Precompute the identity of every block we care about.
        let mut kinds = HashMap::new();
        for (names, kind) in [(ORES, BlockKind::Ore), (CONTAINERS, BlockKind::Container)] {
            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 }])
                {
                    kinds.insert(hash, kind);
                }
                j.delete_local(block);
            }
        }
        if kinds.is_empty() {
            missing.push("Blocks.* constants".into());
            return None;
        }

        Some(Blocks {
            block_pos_class,
            m_block_pos_init,
            m_get_block_state,
            m_get_block,
            m_identity_hash,
            system_class,
            kinds,
            origin: (0, 0, 0),
            cursor: 0,
            radius: 0,
            partial: Vec::new(),
            found: Vec::new(),
        })
    }

    /// Advance the scan. `budget` positions are read per call, so the cost per
    /// frame stays flat regardless of how big the search volume is.
    pub fn step(
        &mut self,
        j: &Jni,
        level: jobject,
        player_pos: (f64, f64, f64),
        radius: i32,
        want_ores: bool,
        want_containers: bool,
        budget: usize,
    ) {
        if !want_ores && !want_containers {
            self.found.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;

        // Start a fresh pass when the volume changed or we drifted out of it.
        if self.cursor >= total || moved > radius / 2 || self.radius != radius {
            if self.cursor >= total {
                std::mem::swap(&mut self.found, &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(kind) = self.kind_at(j, level, x, y, z) {
                let wanted = match kind {
                    BlockKind::Ore => want_ores,
                    BlockKind::Container => want_containers,
                };
                if wanted {
                    self.partial.push((x, y, z, kind));
                }
            }
        }
        self.cursor = end;
    }

    fn kind_at(&self, j: &Jni, level: jobject, x: i32, y: i32, z: i32) -> Option<BlockKind> {
        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.kinds.get(&hash?).copied()
    }
}

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