Sign in Sign up
kretrod/lodestone Public
Branches
master
512 lines (471 loc) · 18.1 KB Raw
//! The Minecraft layer: find the game's own objects, read state, apply cheats.
//!
//! Two rules shape everything here.
//!
//! 1. **Single player only.** Every write is gated on the integrated server
//!    being live (`Minecraft.singleplayerServer != null`). On a multiplayer
//!    server the trainer reads but never writes.
//! 2. **Primitives only.** We write doubles, floats, ints and booleans. We
//!    never write an object reference: the JIT emits GC barriers around those,
//!    and forging a reference from outside without them corrupts the heap.
//!
//! Object addresses are never cached across ticks — a GC can move any of them.
//! Every tick re-walks from `Minecraft.instance`, which is a static field in a
//! class mirror and therefore a stable root.

use crate::jvm::{JVal, Jvm};
use crate::win::{Process, Result};

const MC: &str = "net/minecraft/client/Minecraft";

/// Vanilla defaults, straight out of `Abilities`.
const VANILLA_FLY_SPEED: f32 = 0.05;
const VANILLA_WALK_SPEED: f32 = 0.1;

/// What the UI can ask for.
#[derive(Debug, Clone)]
pub struct Cheats {
    pub fly: bool,
    pub fly_speed: f32,
    pub speed: bool,
    pub walk_speed: f32,
    pub noclip: bool,
    pub nofall: bool,
    pub god: bool,
    pub instabuild: bool,
    pub fast_break: bool,
    pub no_hurt_cam: bool,
    pub freeze_time: bool,
    /// Multiplier on the world clock: 1.0 is normal, 0 stops it.
    pub time_rate: Option<f32>,
    /// One-shot requests, cleared by the engine once applied.
    pub set_time: Option<i64>,
    pub teleport: Option<(f64, f64, f64)>,
}

impl Default for Cheats {
    fn default() -> Self {
        Self {
            fly: false,
            fly_speed: 0.05,
            speed: false,
            walk_speed: 0.1,
            noclip: false,
            nofall: false,
            god: false,
            instabuild: false,
            fast_break: false,
            no_hurt_cam: false,
            freeze_time: false,
            time_rate: None,
            set_time: None,
            teleport: None,
        }
    }
}

/// What the UI shows.
#[derive(Debug, Clone, Default)]
pub struct Snapshot {
    pub pid: u32,
    pub in_world: bool,
    pub single_player: bool,
    pub pos: (f64, f64, f64),
    pub yaw: f32,
    pub pitch: f32,
    pub on_ground: bool,
    pub flying: bool,
    pub may_fly: bool,
    pub fly_speed: f32,
    pub walk_speed: f32,
    pub fall_distance: f64,
    pub day_time: i64,
    pub time_rate: f32,
    pub time_paused: bool,
    pub has_clock: bool,
    pub player_class: String,
    pub server_player: bool,
    pub note: String,
}

/// Tracks what a module changed, so turning it off puts the game back.
#[derive(Default)]
struct Saved {
    may_fly: Option<bool>,
    fly_speed: Option<f32>,
    walk_speed: Option<f32>,
    invulnerable: Option<bool>,
    instabuild: Option<bool>,
    noclip: Option<bool>,
    paused_clock: Option<bool>,
}

pub struct Game {
    pub j: Jvm,
    k_minecraft: u64,
    /// Shared immutable Vec3 constants — mutating one would corrupt every
    /// other user of it, so writes into these are refused.
    vec3_consts: Vec<u64>,
    saved: Saved,
}

impl Game {
    pub fn attach(pid: Option<u32>) -> Result<Self> {
        let p = match pid {
            Some(pid) => Process::attach(pid)?,
            None => attach_minecraft()?,
        };
        let m = p.module("jvm.dll")?;
        let mut j = Jvm::attach(p, &m)?;
        j.load_classes()?;
        let k_minecraft = j
            .class(MC)
            .ok_or("net.minecraft.client.Minecraft is not loaded — is this Minecraft?")?;

        // Vec3.ZERO and the axis vectors are singletons handed out all over the
        // game; never write through one.
        let mut vec3_consts = Vec::new();
        if let Some(k) = j.class("net/minecraft/world/phys/Vec3") {
            for name in ["ZERO", "X_AXIS", "Y_AXIS", "Z_AXIS"] {
                if let Ok(JVal::Obj(a)) = j.static_field(k, name) {
                    vec3_consts.push(a);
                }
            }
        }
        Ok(Self { j, k_minecraft, vec3_consts, saved: Saved::default() })
    }

    pub fn pid(&self) -> u32 {
        self.j.p.pid()
    }

    /// Re-resolve the client singleton. Cheap, and correct across a GC.
    fn minecraft(&mut self) -> Result<u64> {
        match self.j.static_field(self.k_minecraft, "instance")? {
            JVal::Obj(0) => Err("Minecraft.instance is null".into()),
            JVal::Obj(a) => Ok(a),
            v => Err(format!("Minecraft.instance is {v}")),
        }
    }

    /// Write only when the value actually differs. The game reads these fields
    /// every frame; pointlessly rewriting the same bytes twenty times a second
    /// is both wasted syscalls and a needless chance to race the game's own
    /// write of that field.
    fn set_if(&mut self, oop: u64, field: &str, want: JVal) -> Result<bool> {
        if oop == 0 {
            return Ok(false);
        }
        if self.j.get(oop, field)? == want {
            return Ok(false);
        }
        self.j.set(oop, field, want)?;
        Ok(true)
    }

    fn get_bool(&mut self, oop: u64, field: &str) -> Option<bool> {
        match self.j.get(oop, field) {
            Ok(JVal::Bool(v)) => Some(v),
            _ => None,
        }
    }

    /// Cheap liveness check: if the client singleton no longer reads, the
    /// process is gone or the handle is stale and we should re-attach.
    pub fn alive(&mut self) -> bool {
        self.j
            .static_field(self.k_minecraft, "instance")
            .is_ok()
    }

    fn obj(&mut self, oop: u64, field: &str) -> Result<u64> {
        match self.j.get(oop, field)? {
            JVal::Obj(a) => Ok(a),
            v => Err(format!("{field} is {v}, not an object")),
        }
    }

    /// The authoritative server-side player. In single player the integrated
    /// server lives in this same JVM, so we can edit the real thing instead of
    /// fighting its corrections.
    fn server_player(&mut self, mc: u64) -> Result<u64> {
        let server = self.obj(mc, "singleplayerServer")?;
        if server == 0 {
            return Ok(0);
        }
        let list = self.obj(server, "playerList")?;
        if list == 0 {
            return Ok(0);
        }
        let players = self.obj(list, "players")?;
        if players == 0 {
            return Ok(0);
        }
        let data = self.obj(players, "elementData")?;
        Ok(self.j.obj_array(data, 1)?.first().copied().unwrap_or(0))
    }

    /// Overwrite a Vec3's components in place. Vec3 is immutable in Java, so
    /// this is only sound for a vector the entity owns exclusively — the game
    /// allocates a fresh one every time it moves.
    fn write_vec3(&mut self, vec: u64, x: f64, y: f64, z: f64) -> Result<()> {
        if vec == 0 {
            return Err("null vector".into());
        }
        if self.vec3_consts.contains(&vec) {
            return Err("refusing to write a shared Vec3 constant".into());
        }
        self.j.set(vec, "x", JVal::Double(x))?;
        self.j.set(vec, "y", JVal::Double(y))?;
        self.j.set(vec, "z", JVal::Double(z))?;
        Ok(())
    }

    /// Move one entity, client- or server-side, keeping the interpolation
    /// origin in step so the player does not smear across the screen.
    fn move_entity(&mut self, entity: u64, x: f64, y: f64, z: f64) -> Result<()> {
        let pos = self.obj(entity, "position")?;
        self.write_vec3(pos, x, y, z)?;
        self.j.set(entity, "xo", JVal::Double(x))?;
        self.j.set(entity, "yo", JVal::Double(y))?;
        self.j.set(entity, "zo", JVal::Double(z))?;
        // xOld/yOld/zOld drive rendering interpolation on the client.
        for (f, v) in [("xOld", x), ("yOld", y), ("zOld", z)] {
            let _ = self.j.set(entity, f, JVal::Double(v));
        }
        Ok(())
    }

    /// The world clock. 26.2 keeps it on the integrated server in a map of
    /// ClockState objects; single-player worlds have one.
    fn clock_state(&mut self, mc: u64) -> Result<u64> {
        let server = self.obj(mc, "singleplayerServer")?;
        if server == 0 {
            return Ok(0);
        }
        let manager = self.obj(server, "clockManager")?;
        if manager == 0 {
            return Ok(0);
        }
        let clocks = self.obj(manager, "clocks")?;
        for (_, v) in self.j.map_entries(clocks)? {
            if v != 0 {
                return Ok(v);
            }
        }
        Ok(0)
    }

    /// One engine tick: read the world, then apply whatever is switched on.
    pub fn tick(&mut self, c: &mut Cheats) -> Snapshot {
        let mut s = Snapshot { pid: self.pid(), ..Default::default() };
        let mc = match self.minecraft() {
            Ok(a) => a,
            Err(e) => {
                s.note = e;
                return s;
            }
        };

        let player = self.obj(mc, "player").unwrap_or(0);
        if player == 0 {
            s.note = "not in a world".into();
            return s;
        }
        s.in_world = true;
        s.player_class = self.j.class_name_of(player).unwrap_or_default();

        let sp = self.server_player(mc).unwrap_or(0);
        s.single_player = sp != 0;
        s.server_player = sp != 0;

        // ---- read state ----------------------------------------------------
        if let Ok(pos) = self.obj(player, "position") {
            if let (Ok(JVal::Double(x)), Ok(JVal::Double(y)), Ok(JVal::Double(z))) = (
                self.j.get(pos, "x"),
                self.j.get(pos, "y"),
                self.j.get(pos, "z"),
            ) {
                s.pos = (x, y, z);
            }
        }
        if let Ok(JVal::Float(v)) = self.j.get(player, "yRot") {
            s.yaw = v;
        }
        if let Ok(JVal::Float(v)) = self.j.get(player, "xRot") {
            s.pitch = v;
        }
        if let Ok(JVal::Bool(v)) = self.j.get(player, "onGround") {
            s.on_ground = v;
        }
        if let Ok(JVal::Double(v)) = self.j.get(player, "fallDistance") {
            s.fall_distance = v;
        }
        let abilities = self.obj(player, "abilities").unwrap_or(0);
        if abilities != 0 {
            if let Ok(JVal::Bool(v)) = self.j.get(abilities, "flying") {
                s.flying = v;
            }
            if let Ok(JVal::Bool(v)) = self.j.get(abilities, "mayfly") {
                s.may_fly = v;
            }
            if let Ok(JVal::Float(v)) = self.j.get(abilities, "flyingSpeed") {
                s.fly_speed = v;
            }
            if let Ok(JVal::Float(v)) = self.j.get(abilities, "walkingSpeed") {
                s.walk_speed = v;
            }
        }
        // 26.2 moved world time out of LevelData into the clock system.
        if let Ok(clock) = self.clock_state(mc) {
            if clock != 0 {
                s.has_clock = true;
                if let Ok(JVal::Long(t)) = self.j.get(clock, "totalTicks") {
                    s.day_time = t;
                }
                if let Ok(JVal::Float(r)) = self.j.get(clock, "rate") {
                    s.time_rate = r;
                }
                if let Ok(JVal::Bool(p)) = self.j.get(clock, "paused") {
                    s.time_paused = p;
                }
            }
        }

        // ---- the gate ------------------------------------------------------
        if !s.single_player {
            s.note = "multiplayer — read only".into();
            c.teleport = None;
            c.set_time = None;
            return s;
        }

        // ---- apply ---------------------------------------------------------
        let mut notes: Vec<String> = Vec::new();
        if let Err(e) = self.apply(mc, player, sp, abilities, c, &mut notes) {
            notes.push(e);
        }
        s.note = notes.join("; ");
        s
    }

    fn apply(
        &mut self,
        mc: u64,
        player: u64,
        sp: u64,
        abilities: u64,
        c: &mut Cheats,
        notes: &mut Vec<String>,
    ) -> Result<()> {
        // Flight: the client honours abilities.flying every tick, so holding
        // the flag set is all it takes.
        if abilities != 0 {
            if c.fly {
                if self.saved.may_fly.is_none() {
                    self.saved.may_fly = Some(self.get_bool(abilities, "mayfly").unwrap_or(false));
                }
                let _ = self.set_if(abilities, "mayfly", JVal::Bool(true));
                let _ = self.set_if(abilities, "flying", JVal::Bool(true));
            } else if let Some(prev) = self.saved.may_fly.take() {
                let _ = self.set_if(abilities, "flying", JVal::Bool(false));
                let _ = self.set_if(abilities, "mayfly", JVal::Bool(prev));
            }

            if c.fly {
                self.saved.fly_speed.get_or_insert(VANILLA_FLY_SPEED);
                let _ = self.set_if(abilities, "flyingSpeed", JVal::Float(c.fly_speed));
            } else if let Some(prev) = self.saved.fly_speed.take() {
                let _ = self.set_if(abilities, "flyingSpeed", JVal::Float(prev));
            }

            if c.speed {
                self.saved.walk_speed.get_or_insert(VANILLA_WALK_SPEED);
                let _ = self.set_if(abilities, "walkingSpeed", JVal::Float(c.walk_speed));
            } else if let Some(prev) = self.saved.walk_speed.take() {
                let _ = self.set_if(abilities, "walkingSpeed", JVal::Float(prev));
            }

            // Instant break is exactly the creative-mode ability bit.
            if c.instabuild {
                if self.saved.instabuild.is_none() {
                    self.saved.instabuild =
                        Some(self.get_bool(abilities, "instabuild").unwrap_or(false));
                }
                let _ = self.set_if(abilities, "instabuild", JVal::Bool(true));
            } else if let Some(prev) = self.saved.instabuild.take() {
                let _ = self.set_if(abilities, "instabuild", JVal::Bool(prev));
            }
        }

        // Noclip: the client stops colliding immediately; the server player
        // needs it too or the server rubber-bands us back out of the wall.
        if c.noclip {
            if self.saved.noclip.is_none() {
                self.saved.noclip = Some(self.get_bool(player, "noPhysics").unwrap_or(false));
            }
            let _ = self.set_if(player, "noPhysics", JVal::Bool(true));
            let _ = self.set_if(sp, "noPhysics", JVal::Bool(true));
        } else if let Some(prev) = self.saved.noclip.take() {
            let _ = self.set_if(player, "noPhysics", JVal::Bool(prev));
            let _ = self.set_if(sp, "noPhysics", JVal::Bool(prev));
        }

        // Fall damage is computed from fallDistance server-side, so zero the
        // server's copy; the client's copy only drives the landing animation.
        if c.nofall {
            let _ = self.set_if(player, "fallDistance", JVal::Double(0.0));
            let _ = self.set_if(sp, "fallDistance", JVal::Double(0.0));
        }

        // Damage is applied on the server, so god mode belongs there.
        if c.god {
            if self.saved.invulnerable.is_none() && sp != 0 {
                self.saved.invulnerable = Some(self.get_bool(sp, "invulnerable").unwrap_or(false));
            }
            let _ = self.set_if(player, "invulnerable", JVal::Bool(true));
            let _ = self.set_if(sp, "invulnerable", JVal::Bool(true));
        } else if let Some(prev) = self.saved.invulnerable.take() {
            let _ = self.set_if(player, "invulnerable", JVal::Bool(prev));
            let _ = self.set_if(sp, "invulnerable", JVal::Bool(prev));
        }

        if c.no_hurt_cam {
            let _ = self.set_if(player, "hurtTime", JVal::Int(0));
        }

        // Mining cooldown between blocks.
        if c.fast_break {
            if let Ok(gm) = self.obj(mc, "gameMode") {
                let _ = self.set_if(gm, "destroyDelay", JVal::Int(0));
            }
        }

        // The world clock is server-side state; the client follows it.
        let clock = self.clock_state(mc).unwrap_or(0);
        if clock != 0 {
            if let Some(t) = c.set_time.take() {
                let _ = self.j.set(clock, "totalTicks", JVal::Long(t));
            }
            if c.freeze_time {
                if self.saved.paused_clock.is_none() {
                    self.saved.paused_clock = Some(self.get_bool(clock, "paused").unwrap_or(false));
                }
                let _ = self.set_if(clock, "paused", JVal::Bool(true));
            } else if let Some(prev) = self.saved.paused_clock.take() {
                let _ = self.set_if(clock, "paused", JVal::Bool(prev));
            }
            if let Some(r) = c.time_rate {
                let _ = self.set_if(clock, "rate", JVal::Float(r));
            }
        } else if c.set_time.take().is_some() || c.freeze_time {
            notes.push("no clock found".into());
        }

        if let Some((x, y, z)) = c.teleport.take() {
            // Server first: when the client's move packet arrives the server
            // already agrees with it, so there is nothing to correct.
            if sp != 0 {
                if let Err(e) = self.move_entity(sp, x, y, z) {
                    notes.push(format!("server teleport: {e}"));
                }
            }
            if let Err(e) = self.move_entity(player, x, y, z) {
                notes.push(format!("client teleport: {e}"));
            }
        }
        Ok(())
    }



}

/// Find the single JVM that has GLFW/LWJGL loaded — that is the game.
pub fn attach_minecraft() -> Result<Process> {
    let mut found = Vec::new();
    for p in crate::win::list_processes()? {
        let n = p.name.to_ascii_lowercase();
        if n != "javaw.exe" && n != "java.exe" {
            continue;
        }
        if let Ok(h) = Process::attach(p.pid) {
            let is_game = h
                .modules()
                .map(|ms| {
                    ms.iter().any(|m| {
                        let n = m.name.to_ascii_lowercase();
                        n.starts_with("lwjgl") || n.starts_with("glfw")
                    })
                })
                .unwrap_or(false);
            if is_game {
                found.push(h);
            }
        }
    }
    match found.len() {
        1 => Ok(found.pop().unwrap()),
        0 => Err("Minecraft is not running".into()),
        n => Err(format!("{n} Minecraft processes are running")),
    }
}